diff --git a/.circleci/config.yml b/.circleci/config.yml index 3fe7d604c70..7d46584e12d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -997,6 +997,16 @@ jobs: aztec_manifest_key: end-to-end <<: *defaults_e2e_test + e2e-account-init-fees: + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_account_init_fees.test.ts ENABLE_GAS=1 + aztec_manifest_key: end-to-end + <<: *defaults_e2e_test + e2e-dapp-subscription: steps: - *checkout @@ -1480,6 +1490,7 @@ workflows: - e2e-card-game: *e2e_test - e2e-avm-simulator: *e2e_test - e2e-fees: *e2e_test + - e2e-account-init-fees: *e2e_test - e2e-dapp-subscription: *e2e_test - pxe: *e2e_test - cli-docs-sandbox: *e2e_test @@ -1546,6 +1557,7 @@ workflows: - e2e-card-game - e2e-avm-simulator - e2e-fees + - e2e-account-init-fees - e2e-dapp-subscription - pxe - boxes-vanilla diff --git a/noir-projects/aztec-nr/authwit/src/account.nr b/noir-projects/aztec-nr/authwit/src/account.nr index 2e33c4a0823..0bc16963e25 100644 --- a/noir-projects/aztec-nr/authwit/src/account.nr +++ b/noir-projects/aztec-nr/authwit/src/account.nr @@ -70,6 +70,16 @@ impl AccountActions { } // docs:end:entrypoint + pub fn pay_init_fee(self, fee_payload: FeePayload) { + let valid_fn = self.is_valid_impl; + let mut private_context = self.context.private.unwrap(); + + let fee_hash = fee_payload.hash(); + assert(valid_fn(private_context, fee_hash)); + fee_payload.execute_calls(private_context); + private_context.capture_min_revertible_side_effect_counter(); + } + // docs:start:spend_private_authwit pub fn spend_private_authwit(self, inner_hash: Field) -> Field { let context = self.context.private.unwrap(); diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index f9de0291c38..c3ec9a75604 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -347,6 +347,19 @@ impl PrivateContext { assert_eq(item.public_inputs.start_side_effect_counter, self.side_effect_counter); self.side_effect_counter = item.public_inputs.end_side_effect_counter + 1; + // TODO (fees) figure out why this crashes the prover and enable it + // we need this in order to pay fees inside child call contexts + // assert( + // (item.public_inputs.min_revertible_side_effect_counter == 0 as u32) + // | (item.public_inputs.min_revertible_side_effect_counter + // > self.min_revertible_side_effect_counter) + // ); + + // if item.public_inputs.min_revertible_side_effect_counter + // > self.min_revertible_side_effect_counter { + // self.min_revertible_side_effect_counter = item.public_inputs.min_revertible_side_effect_counter; + // } + assert(contract_address.eq(item.contract_address)); assert(function_selector.eq(item.function_data.selector)); @@ -370,6 +383,8 @@ impl PrivateContext { ); } + // crate::oracle::debug_log::debug_log_array_with_prefix("Private call stack item", item.serialize()); + self.private_call_stack_hashes.push(item.hash()); item.public_inputs.return_values diff --git a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr index d7da24dbf67..e0cd1cdaf90 100644 --- a/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/ecdsa_account_contract/src/main.nr @@ -40,6 +40,14 @@ contract EcdsaAccount { } #[aztec(private)] + #[aztec(noinitcheck)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + + #[aztec(private)] + #[aztec(noinitcheck)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); actions.spend_private_authwit(inner_hash) diff --git a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr index bc1d49931d8..f0b3f2c8ddd 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_account_contract/src/main.nr @@ -38,6 +38,7 @@ contract SchnorrAccount { // Note: If you globally change the entrypoint signature don't forget to update default_entrypoint.ts file #[aztec(private)] + #[aztec(noinitcheck)] fn entrypoint(app_payload: pub AppPayload, fee_payload: pub FeePayload) { let actions = AccountActions::private( &mut context, @@ -48,6 +49,18 @@ contract SchnorrAccount { } #[aztec(private)] + #[aztec(noinitcheck)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private( + &mut context, + storage.approved_actions.storage_slot, + is_valid_impl + ); + actions.pay_init_fee(fee_payload); + } + + #[aztec(private)] + #[aztec(noinitcheck)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private( &mut context, @@ -58,6 +71,7 @@ contract SchnorrAccount { } #[aztec(public)] + #[aztec(noinitcheck)] fn spend_public_authwit(inner_hash: Field) -> Field { let actions = AccountActions::public( &mut context, @@ -75,6 +89,7 @@ contract SchnorrAccount { #[aztec(public)] #[aztec(internal)] + #[aztec(noinitcheck)] fn approve_public_authwit(outer_hash: Field) { let actions = AccountActions::public( &mut context, @@ -118,9 +133,9 @@ contract SchnorrAccount { * @param block_number The block number to check the nullifier against * @param check_private Whether to check the validity of the authwitness in private state or not * @param message_hash The message hash of the message to check the validity - * @return An array of two booleans, the first is the validity of the authwitness in the private state, + * @return An array of two booleans, the first is the validity of the authwitness in the private state, * the second is the validity of the authwitness in the public state - * Both values will be `false` if the nullifier is spent + * Both values will be `false` if the nullifier is spent */ unconstrained fn lookup_validity( myself: AztecAddress, @@ -148,7 +163,7 @@ contract SchnorrAccount { let valid_in_public = storage.approved_actions.at(message_hash).read(); // Compute the nullifier and check if it is spent - // This will BLINDLY TRUST the oracle, but the oracle is us, and + // This will BLINDLY TRUST the oracle, but the oracle is us, and // it is not as part of execution of the contract, so we are good. let siloed_nullifier = compute_siloed_nullifier(myself, message_hash); let lower_wit = get_low_nullifier_membership_witness(block_number, siloed_nullifier); diff --git a/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr index a660670a2a5..134a820b058 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_hardcoded_account_contract/src/main.nr @@ -21,6 +21,12 @@ contract SchnorrHardcodedAccount { actions.entrypoint(app_payload, fee_payload); } + #[aztec(private)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + #[aztec(private)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); diff --git a/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr index 9803ed4f15e..2a8678ce778 100644 --- a/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/schnorr_single_key_account_contract/src/main.nr @@ -17,6 +17,12 @@ contract SchnorrSingleKeyAccount { actions.entrypoint(app_payload, fee_payload); } + #[aztec(private)] + fn pay_init_fee(fee_payload: pub FeePayload) { + let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); + actions.pay_init_fee(fee_payload); + } + #[aztec(private)] fn spend_private_authwit(inner_hash: Field) -> Field { let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl); diff --git a/yarn-project/accounts/src/defaults/account_interface.ts b/yarn-project/accounts/src/defaults/account_interface.ts index 44e68db64e2..5d7fa311c6e 100644 --- a/yarn-project/accounts/src/defaults/account_interface.ts +++ b/yarn-project/accounts/src/defaults/account_interface.ts @@ -1,6 +1,6 @@ import { type AccountInterface, type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface, type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { type AuthWitness, type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { type AuthWitness, type TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, type CompleteAddress, Fr } from '@aztec/circuits.js'; import { DefaultAccountEntrypoint } from '@aztec/entrypoints/account'; import { type NodeInfo } from '@aztec/types/interfaces'; @@ -29,8 +29,8 @@ export class DefaultAccountInterface implements AccountInterface { this.version = new Fr(nodeInfo.protocolVersion); } - createTxExecutionRequest(executions: FunctionCall[], fee?: FeeOptions): Promise { - return this.entrypoint.createTxExecutionRequest(executions, fee); + createTxExecutionRequest(execution: ExecutionRequestInit): Promise { + return this.entrypoint.createTxExecutionRequest(execution); } createAuthWit(messageHash: Fr): Promise { diff --git a/yarn-project/aztec.js/src/account/contract.ts b/yarn-project/aztec.js/src/account/contract.ts index 0711292bbac..6c49a3b5cf0 100644 --- a/yarn-project/aztec.js/src/account/contract.ts +++ b/yarn-project/aztec.js/src/account/contract.ts @@ -2,7 +2,7 @@ import { type CompleteAddress } from '@aztec/circuit-types'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { type NodeInfo } from '@aztec/types/interfaces'; -import { type AccountInterface } from './interface.js'; +import { type AccountInterface, type AuthWitnessProvider } from './interface.js'; // docs:start:account-contract-interface /** @@ -29,5 +29,11 @@ export interface AccountContract { * @returns An account interface instance for creating tx requests and authorizing actions. */ getInterface(address: CompleteAddress, nodeInfo: NodeInfo): AccountInterface; + + /** + * Returns the auth witness provider for the given address. + * @param address - Address for which to create auth witnesses. + */ + getAuthWitnessProvider(address: CompleteAddress): AuthWitnessProvider; } // docs:end:account-contract-interface diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts new file mode 100644 index 00000000000..75a2c6bd0d1 --- /dev/null +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_method.ts @@ -0,0 +1,71 @@ +import { type PublicKey } from '@aztec/circuit-types'; +import { FunctionData } from '@aztec/circuits.js'; +import { + type ContractArtifact, + type FunctionArtifact, + encodeArguments, + getFunctionArtifact, +} from '@aztec/foundation/abi'; + +import { type AuthWitnessProvider } from '../account/interface.js'; +import { type Wallet } from '../account/wallet.js'; +import { type ExecutionRequestInit } from '../api/entrypoint.js'; +import { Contract } from '../contract/contract.js'; +import { DeployMethod, type DeployOptions } from '../contract/deploy_method.js'; +import { EntrypointPayload } from '../entrypoint/payload.js'; + +/** + * Contract interaction for deploying an account contract. Handles fee preparation and contract initialization. + */ +export class DeployAccountMethod extends DeployMethod { + #authWitnessProvider: AuthWitnessProvider; + #feePaymentArtifact: FunctionArtifact | undefined; + + constructor( + authWitnessProvider: AuthWitnessProvider, + publicKey: PublicKey, + wallet: Wallet, + artifact: ContractArtifact, + args: any[] = [], + constructorNameOrArtifact?: string | FunctionArtifact, + feePaymentNameOrArtifact?: string | FunctionArtifact, + ) { + super( + publicKey, + wallet, + artifact, + (address, wallet) => Contract.at(address, artifact, wallet), + args, + constructorNameOrArtifact, + ); + + this.#authWitnessProvider = authWitnessProvider; + this.#feePaymentArtifact = + typeof feePaymentNameOrArtifact === 'string' + ? getFunctionArtifact(artifact, feePaymentNameOrArtifact) + : feePaymentNameOrArtifact; + } + + protected async getInitializeFunctionCalls(options: DeployOptions): Promise { + const exec = await super.getInitializeFunctionCalls(options); + + if (options.fee && this.#feePaymentArtifact) { + const { address } = this.getInstance(); + const feePayload = await EntrypointPayload.fromFeeOptions(options?.fee); + + exec.calls.push({ + to: address, + args: encodeArguments(this.#feePaymentArtifact, [feePayload]), + functionData: FunctionData.fromAbi(this.#feePaymentArtifact), + }); + + exec.authWitnesses ??= []; + exec.packedArguments ??= []; + + exec.authWitnesses.push(await this.#authWitnessProvider.createAuthWit(feePayload.hash())); + exec.packedArguments.push(...feePayload.packedArguments); + } + + return exec; + } +} diff --git a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts index cfaa577ea8e..ad299a68b5b 100644 --- a/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts +++ b/yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts @@ -1,4 +1,4 @@ -import { type TxHash, type TxReceipt } from '@aztec/circuit-types'; +import { type PXE, type TxHash, type TxReceipt } from '@aztec/circuit-types'; import { type FieldsOf } from '@aztec/foundation/types'; import { type Wallet } from '../account/index.js'; @@ -15,8 +15,8 @@ export type DeployAccountTxReceipt = FieldsOf & { * A deployment transaction for an account contract sent to the network, extending SentTx with methods to get the resulting wallet. */ export class DeployAccountSentTx extends SentTx { - constructor(private wallet: Wallet, txHashPromise: Promise) { - super(wallet, txHashPromise); + constructor(pxe: PXE, txHashPromise: Promise, private getWalletPromise: Promise) { + super(pxe, txHashPromise); } /** @@ -36,7 +36,8 @@ export class DeployAccountSentTx extends SentTx { */ public async wait(opts: WaitOpts = DefaultWaitOpts): Promise { const receipt = await super.wait(opts); - await waitForAccountSynch(this.pxe, this.wallet.getCompleteAddress(), opts); - return { ...receipt, wallet: this.wallet }; + const wallet = await this.getWalletPromise; + await waitForAccountSynch(this.pxe, wallet.getCompleteAddress(), opts); + return { ...receipt, wallet }; } } diff --git a/yarn-project/aztec.js/src/account_manager/index.ts b/yarn-project/aztec.js/src/account_manager/index.ts index d18bc8b38d2..a8c96b6887f 100644 --- a/yarn-project/aztec.js/src/account_manager/index.ts +++ b/yarn-project/aztec.js/src/account_manager/index.ts @@ -6,14 +6,20 @@ import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type AccountContract } from '../account/contract.js'; import { type Salt } from '../account/index.js'; import { type AccountInterface } from '../account/interface.js'; -import { type DeployMethod } from '../contract/deploy_method.js'; +import { type DeployOptions } from '../contract/deploy_method.js'; import { DefaultWaitOpts, type WaitOpts } from '../contract/sent_tx.js'; -import { ContractDeployer } from '../deployment/contract_deployer.js'; +import { DefaultMultiCallEntrypoint } from '../entrypoint/default_multi_call_entrypoint.js'; import { waitForAccountSynch } from '../utils/account.js'; import { generatePublicKey } from '../utils/index.js'; import { AccountWalletWithPrivateKey, SignerlessWallet } from '../wallet/index.js'; +import { DeployAccountMethod } from './deploy_account_method.js'; import { DeployAccountSentTx } from './deploy_account_sent_tx.js'; +/** + * Options to deploy an account contract. + */ +export type DeployAccountOptions = Pick; + /** * Manages a user account. Provides methods for calculating the account's address, deploying the account contract, * and creating and registering the user wallet in the PXE Service. @@ -26,7 +32,7 @@ export class AccountManager { private completeAddress?: CompleteAddress; private instance?: ContractInstanceWithAddress; private encryptionPublicKey?: PublicKey; - private deployMethod?: DeployMethod; + private deployMethod?: DeployAccountMethod; constructor( private pxe: PXE, @@ -128,17 +134,22 @@ export class AccountManager { } await this.#register(); const encryptionPublicKey = this.getEncryptionPublicKey(); - // We use a signerless wallet so we hit the account contract directly and it deploys itself. + const { chainId, protocolVersion } = await this.pxe.getNodeInfo(); + const deployWallet = new SignerlessWallet(this.pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion)); + + // We use a signerless wallet with the multi call entrypoint in order to make multiple calls in one go // If we used getWallet, the deployment would get routed via the account contract entrypoint - // instead of directly hitting the initializer. - const deployWallet = new SignerlessWallet(this.pxe); - const deployer = new ContractDeployer( - this.accountContract.getContractArtifact(), - deployWallet, + // and it can't be used unless the contract is initialized + const args = this.accountContract.getDeploymentArgs() ?? []; + this.deployMethod = new DeployAccountMethod( + this.accountContract.getAuthWitnessProvider(this.getCompleteAddress()), encryptionPublicKey, + deployWallet, + this.accountContract.getContractArtifact(), + args, + 'constructor', + 'pay_init_fee', ); - const args = this.accountContract.getDeploymentArgs() ?? []; - this.deployMethod = deployer.deploy(...args); } return this.deployMethod; } @@ -148,18 +159,23 @@ export class AccountManager { * Does not register the associated class nor publicly deploy the instance by default. * Uses the salt provided in the constructor or a randomly generated one. * Registers the account in the PXE Service before deploying the contract. + * @param opts - Fee options to be used for the deployment. * @returns A SentTx object that can be waited to get the associated Wallet. */ - public async deploy(): Promise { - const deployMethod = await this.getDeployMethod(); - const wallet = await this.getWallet(); - const sentTx = deployMethod.send({ - contractAddressSalt: this.salt, - skipClassRegistration: true, - skipPublicDeployment: true, - universalDeploy: true, - }); - return new DeployAccountSentTx(wallet, sentTx.getTxHash()); + public deploy(opts?: DeployAccountOptions): DeployAccountSentTx { + const sentTx = this.getDeployMethod() + .then(deployMethod => + deployMethod.send({ + contractAddressSalt: this.salt, + skipClassRegistration: opts?.skipClassRegistration ?? true, + skipPublicDeployment: opts?.skipPublicDeployment ?? true, + skipInitialization: false, + universalDeploy: true, + fee: opts?.fee, + }), + ) + .then(tx => tx.getTxHash()); + return new DeployAccountSentTx(this.pxe, sentTx, this.getWallet()); } /** @@ -170,7 +186,7 @@ export class AccountManager { * @returns A Wallet instance. */ public async waitSetup(opts: WaitOpts = DefaultWaitOpts): Promise { - await (this.isDeployable() ? this.deploy().then(tx => tx.wait(opts)) : this.register()); + await (this.isDeployable() ? this.deploy().wait(opts) : this.register()); return this.getWallet(); } diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index b6a9c29f5c5..79cead71f4d 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -17,7 +17,10 @@ export class BatchCall extends BaseContractInteraction { */ public async create(opts?: SendMethodOptions): Promise { if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest(this.calls, opts?.fee); + this.txRequest = await this.wallet.createTxExecutionRequest({ + calls: this.calls, + fee: opts?.fee, + }); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index a79e8c652f4..be919ff40b4 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -47,7 +47,10 @@ export class ContractFunctionInteraction extends BaseContractInteraction { throw new Error("Can't call `create` on an unconstrained function."); } if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest([this.request()], opts?.fee); + this.txRequest = await this.wallet.createTxExecutionRequest({ + calls: [this.request()], + fee: opts?.fee, + }); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index eea033bce7f..32007df8945 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -14,6 +14,7 @@ import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { type Wallet } from '../account/index.js'; import { deployInstance } from '../deployment/deploy_instance.js'; import { registerContractClass } from '../deployment/register_class.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { BaseContractInteraction, type SendMethodOptions } from './base_contract_interaction.js'; import { type Contract } from './contract.js'; import { type ContractBase } from './contract_base.js'; @@ -53,7 +54,7 @@ export class DeployMethod extends Bas private constructorArtifact: FunctionArtifact | undefined; /** Cached call to request() */ - private functionCalls: FunctionCall[] | undefined; + private functionCalls?: ExecutionRequestInit; private log = createDebugLogger('aztec:js:deploy_method'); @@ -80,10 +81,6 @@ export class DeployMethod extends Bas */ public async create(options: DeployOptions = {}): Promise { if (!this.txRequest) { - const calls = await this.request(options); - if (calls.length === 0) { - throw new Error(`No function calls needed to deploy contract ${this.artifact.name}`); - } this.txRequest = await this.wallet.createTxExecutionRequest(await this.request(options)); // TODO: Should we add the contracts to the DB here, or once the tx has been sent or mined? await this.pxe.registerContract({ artifact: this.artifact, instance: this.instance! }); @@ -99,20 +96,21 @@ export class DeployMethod extends Bas * @remarks This method does not have the same return type as the `request` in the ContractInteraction object, * it returns a promise for an array instead of a function call directly. */ - public async request(options: DeployOptions = {}): Promise { + public async request(options: DeployOptions = {}): Promise { if (!this.functionCalls) { - const { address } = this.getInstance(options); - const calls = await this.getDeploymentFunctionCalls(options); - if (this.constructorArtifact && !options.skipInitialization) { - const constructorCall = new ContractFunctionInteraction( - this.wallet, - address, - this.constructorArtifact, - this.args, - ); - calls.push(constructorCall.request()); + const deployment = await this.getDeploymentFunctionCalls(options); + const bootstrap = await this.getInitializeFunctionCalls(options); + + if (deployment.calls.length + bootstrap.calls.length === 0) { + throw new Error(`No function calls needed to deploy contract ${this.artifact.name}`); } - this.functionCalls = calls; + + this.functionCalls = { + calls: [...deployment.calls, ...bootstrap.calls], + authWitnesses: [...(deployment.authWitnesses ?? []), ...(bootstrap.authWitnesses ?? [])], + packedArguments: [...(deployment.packedArguments ?? []), ...(bootstrap.packedArguments ?? [])], + fee: options.fee, + }; } return this.functionCalls; } @@ -122,7 +120,7 @@ export class DeployMethod extends Bas * @param options - Deployment options. * @returns A function call array with potentially requests to the class registerer and instance deployer. */ - protected async getDeploymentFunctionCalls(options: DeployOptions = {}): Promise { + protected async getDeploymentFunctionCalls(options: DeployOptions = {}): Promise { const calls: FunctionCall[] = []; // Set contract instance object so it's available for populating the DeploySendTx object @@ -156,7 +154,31 @@ export class DeployMethod extends Bas calls.push(deployInstance(this.wallet, instance).request()); } - return calls; + return { + calls, + }; + } + + /** + * Returns the calls necessary to initialize the contract. + * @param options - Deployment options. + * @returns - An array of function calls. + */ + protected getInitializeFunctionCalls(options: DeployOptions): Promise { + const { address } = this.getInstance(options); + const calls: FunctionCall[] = []; + if (this.constructorArtifact && !options.skipInitialization) { + const constructorCall = new ContractFunctionInteraction( + this.wallet, + address, + this.constructorArtifact, + this.args, + ); + calls.push(constructorCall.request()); + } + return Promise.resolve({ + calls, + }); } /** diff --git a/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts index a88cfef9e7a..e7a43da5903 100644 --- a/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/default_entrypoint.ts @@ -1,7 +1,7 @@ -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { TxContext } from '@aztec/circuits.js'; -import { type EntrypointInterface } from './entrypoint.js'; +import { type EntrypointInterface, type ExecutionRequestInit } from './entrypoint.js'; /** * Default implementation of the entrypoint interface. It calls a function on a contract directly @@ -9,18 +9,24 @@ import { type EntrypointInterface } from './entrypoint.js'; export class DefaultEntrypoint implements EntrypointInterface { constructor(private chainId: number, private protocolVersion: number) {} - createTxExecutionRequest(executions: FunctionCall[]): Promise { - const [execution] = executions; - const packedArguments = PackedArguments.fromArgs(execution.args); + createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls, authWitnesses = [], packedArguments = [] } = exec; + + if (calls.length > 1) { + throw new Error(`Expected a single call, got ${calls.length}`); + } + + const call = calls[0]; + const entrypointPackedArguments = PackedArguments.fromArgs(call.args); const txContext = TxContext.empty(this.chainId, this.protocolVersion); return Promise.resolve( new TxExecutionRequest( - execution.to, - execution.functionData, - packedArguments.hash, + call.to, + call.functionData, + entrypointPackedArguments.hash, txContext, - [packedArguments], - [], + [...packedArguments, entrypointPackedArguments], + authWitnesses, ), ); } diff --git a/yarn-project/entrypoints/src/multi_call_entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts similarity index 79% rename from yarn-project/entrypoints/src/multi_call_entrypoint.ts rename to yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts index 86e3d82e0ea..1156e733fae 100644 --- a/yarn-project/entrypoints/src/multi_call_entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/default_multi_call_entrypoint.ts @@ -1,35 +1,32 @@ -import { type EntrypointInterface } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { getCanonicalMultiCallEntrypointAddress } from '@aztec/protocol-contracts/multi-call-entrypoint'; -import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildAppPayload } from './entrypoint_payload.js'; - /** * Implementation for an entrypoint interface that can execute multiple function calls in a single transaction */ export class DefaultMultiCallEntrypoint implements EntrypointInterface { constructor( + private chainId: number, + private version: number, private address: AztecAddress = getCanonicalMultiCallEntrypointAddress(), - private chainId: number = DEFAULT_CHAIN_ID, - private version: number = DEFAULT_VERSION, ) {} - createTxExecutionRequest(executions: FunctionCall[]): Promise { - const { payload: appPayload, packedArguments: appPackedArguments } = buildAppPayload(executions); - + createTxExecutionRequest(executions: ExecutionRequestInit): Promise { + const { calls, authWitnesses = [], packedArguments = [] } = executions; + const payload = EntrypointPayload.fromAppExecution(calls); const abi = this.getEntrypointAbi(); - const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [appPayload])); + const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [payload])); const txRequest = TxExecutionRequest.from({ argsHash: entrypointPackedArgs.hash, origin: this.address, functionData: FunctionData.fromAbi(abi), txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...appPackedArguments, entrypointPackedArgs], - authWitnesses: [], + packedArguments: [...payload.packedArguments, ...packedArguments, entrypointPackedArgs], + authWitnesses, }); return Promise.resolve(txRequest); diff --git a/yarn-project/aztec.js/src/entrypoint/entrypoint.ts b/yarn-project/aztec.js/src/entrypoint/entrypoint.ts index ece23e67924..9cc6618c04f 100644 --- a/yarn-project/aztec.js/src/entrypoint/entrypoint.ts +++ b/yarn-project/aztec.js/src/entrypoint/entrypoint.ts @@ -1,25 +1,35 @@ -import { type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types'; -import { type Fr } from '@aztec/foundation/fields'; +import { + type AuthWitness, + type FunctionCall, + type PackedArguments, + type TxExecutionRequest, +} from '@aztec/circuit-types'; -import { type FeePaymentMethod } from '../fee/fee_payment_method.js'; +import { EntrypointPayload, type FeeOptions } from './payload.js'; -/** - * Fee payment options for a transaction. - */ -export type FeeOptions = { - /** The fee payment method to use */ - paymentMethod: FeePaymentMethod; - /** The fee limit to pay */ - maxFee: bigint | number | Fr; +export { EntrypointPayload, FeeOptions }; + +export { DefaultEntrypoint } from './default_entrypoint.js'; +export { DefaultMultiCallEntrypoint } from './default_multi_call_entrypoint.js'; + +/** Encodes the calls to be done in a transaction. */ +export type ExecutionRequestInit = { + /** The function calls to be executed. */ + calls: FunctionCall[]; + /** Any transient auth witnesses needed for this execution */ + authWitnesses?: AuthWitness[]; + /** Any transient packed arguments for this execution */ + packedArguments?: PackedArguments[]; + /** How the fee is going to be payed */ + fee?: FeeOptions; }; /** Creates transaction execution requests out of a set of function calls. */ export interface EntrypointInterface { /** * Generates an execution request out of set of function calls. - * @param executions - The execution intents to be run. - * @param feeOpts - The fee to be paid for the transaction. + * @param execution - The execution intents to be run. * @returns The authenticated transaction execution request. */ - createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise; + createTxExecutionRequest(execution: ExecutionRequestInit): Promise; } diff --git a/yarn-project/aztec.js/src/entrypoint/payload.ts b/yarn-project/aztec.js/src/entrypoint/payload.ts new file mode 100644 index 00000000000..83457a27b07 --- /dev/null +++ b/yarn-project/aztec.js/src/entrypoint/payload.ts @@ -0,0 +1,144 @@ +import { type FunctionCall, PackedArguments, emptyFunctionCall } from '@aztec/circuit-types'; +import { Fr, GeneratorIndex } from '@aztec/circuits.js'; +import { padArrayEnd } from '@aztec/foundation/collection'; +import { pedersenHash } from '@aztec/foundation/crypto'; +import { type Tuple } from '@aztec/foundation/serialize'; + +import { type FeePaymentMethod } from '../fee/fee_payment_method.js'; + +/** + * Fee payment options for a transaction. + */ +export type FeeOptions = { + /** The fee payment method to use */ + paymentMethod: FeePaymentMethod; + /** The fee limit to pay */ + maxFee: bigint | number | Fr; +}; + +// These must match the values defined in: +// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr +const APP_MAX_CALLS = 4; +// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr +const FEE_MAX_CALLS = 2; + +/* eslint-disable camelcase */ +/** Encoded function call for account contract entrypoint */ +type EncodedFunctionCall = { + /** Arguments hash for the call */ + args_hash: Fr; + /** Selector of the function to call */ + function_selector: Fr; + /** Address of the contract to call */ + target_address: Fr; + /** Whether the function is public or private */ + is_public: boolean; +}; +/* eslint-enable camelcase */ + +/** Assembles an entrypoint payload */ +export class EntrypointPayload { + #packedArguments: PackedArguments[] = []; + #functionCalls: EncodedFunctionCall[] = []; + #nonce = Fr.random(); + #generatorIndex: number; + + private constructor(functionCalls: FunctionCall[], generatorIndex: number) { + for (const call of functionCalls) { + this.#packedArguments.push(PackedArguments.fromArgs(call.args)); + } + + /* eslint-disable camelcase */ + this.#functionCalls = functionCalls.map((call, index) => ({ + args_hash: this.#packedArguments[index].hash, + function_selector: call.functionData.selector.toField(), + target_address: call.to.toField(), + is_public: !call.functionData.isPrivate, + })); + /* eslint-enable camelcase */ + + this.#generatorIndex = generatorIndex; + } + + /* eslint-disable camelcase */ + /** + * The function calls to execute. This uses snake_case naming so that it is compatible with Noir encoding + * @internal + */ + get function_calls() { + return this.#functionCalls; + } + /* eslint-enable camelcase */ + + /** + * The nonce + * @internal + */ + get nonce() { + return this.#nonce; + } + + /** + * The packed arguments for the function calls + */ + get packedArguments() { + return this.#packedArguments; + } + + /** + * Serializes the payload to an array of fields + * @returns The fields of the payload + */ + toFields(): Fr[] { + return [ + ...this.#functionCalls.flatMap(call => [ + call.args_hash, + call.function_selector, + call.target_address, + new Fr(call.is_public), + ]), + this.#nonce, + ]; + } + + /** + * Hashes the payload + * @returns The hash of the payload + */ + hash() { + return pedersenHash(this.toFields(), this.#generatorIndex); + } + + /** + * Creates an execution payload from a set of function calls + * @param functionCalls - The function calls to execute + * @returns The execution payload + */ + static fromFunctionCalls(functionCalls: FunctionCall[]) { + return new EntrypointPayload(functionCalls, 0); + } + + /** + * Creates an execution payload for the app-portion of a transaction from a set of function calls + * @param functionCalls - The function calls to execute + * @returns The execution payload + */ + static fromAppExecution(functionCalls: FunctionCall[] | Tuple) { + if (functionCalls.length > APP_MAX_CALLS) { + throw new Error(`Expected at most ${APP_MAX_CALLS} function calls, got ${functionCalls.length}`); + } + const paddedCalls = padArrayEnd(functionCalls, emptyFunctionCall(), APP_MAX_CALLS); + return new EntrypointPayload(paddedCalls, GeneratorIndex.SIGNATURE_PAYLOAD); + } + + /** + * Creates an execution payload to pay the fee for a transaction + * @param feeOpts - The fee payment options + * @returns The execution payload + */ + static async fromFeeOptions(feeOpts?: FeeOptions) { + const calls = feeOpts ? await feeOpts.paymentMethod.getFunctionCalls(new Fr(feeOpts.maxFee)) : []; + const paddedCalls = padArrayEnd(calls, emptyFunctionCall(), FEE_MAX_CALLS); + return new EntrypointPayload(paddedCalls, GeneratorIndex.FEE_PAYLOAD); + } +} diff --git a/yarn-project/aztec.js/src/wallet/account_wallet.ts b/yarn-project/aztec.js/src/wallet/account_wallet.ts index b75c81fca70..40ac9986477 100644 --- a/yarn-project/aztec.js/src/wallet/account_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/account_wallet.ts @@ -4,7 +4,7 @@ import { type ABIParameterVisibility, type FunctionAbi, FunctionType } from '@az import { type AccountInterface } from '../account/interface.js'; import { ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; -import { type FeeOptions } from '../entrypoint/entrypoint.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { computeAuthWitMessageHash } from '../utils/authwit.js'; import { BaseWallet } from './base_wallet.js'; @@ -16,8 +16,8 @@ export class AccountWallet extends BaseWallet { super(pxe); } - createTxExecutionRequest(execs: FunctionCall[], fee?: FeeOptions): Promise { - return this.account.createTxExecutionRequest(execs, fee); + createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + return this.account.createTxExecutionRequest(exec); } getChainId(): Fr { diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index e5d5775eaa9..70106a6da67 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -28,7 +28,7 @@ import { type NodeInfo } from '@aztec/types/interfaces'; import { type Wallet } from '../account/wallet.js'; import { type ContractFunctionInteraction } from '../contract/contract_function_interaction.js'; -import { type FeeOptions } from '../entrypoint/entrypoint.js'; +import { type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; /** * A base class for Wallet implementations @@ -42,7 +42,7 @@ export abstract class BaseWallet implements Wallet { abstract getVersion(): Fr; - abstract createTxExecutionRequest(execs: FunctionCall[], fee?: FeeOptions): Promise; + abstract createTxExecutionRequest(exec: ExecutionRequestInit): Promise; abstract createAuthWit( messageHashOrIntent: diff --git a/yarn-project/aztec.js/src/wallet/signerless_wallet.ts b/yarn-project/aztec.js/src/wallet/signerless_wallet.ts index 63ef84ae128..062c4491956 100644 --- a/yarn-project/aztec.js/src/wallet/signerless_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/signerless_wallet.ts @@ -1,8 +1,8 @@ -import { type AuthWitness, type FunctionCall, type PXE, type TxExecutionRequest } from '@aztec/circuit-types'; +import { type AuthWitness, type PXE, type TxExecutionRequest } from '@aztec/circuit-types'; import { type CompleteAddress, type Fr } from '@aztec/circuits.js'; import { DefaultEntrypoint } from '../entrypoint/default_entrypoint.js'; -import { type EntrypointInterface } from '../entrypoint/entrypoint.js'; +import { type EntrypointInterface, type ExecutionRequestInit } from '../entrypoint/entrypoint.js'; import { BaseWallet } from './base_wallet.js'; /** @@ -13,14 +13,14 @@ export class SignerlessWallet extends BaseWallet { super(pxe); } - async createTxExecutionRequest(executions: FunctionCall[]): Promise { + async createTxExecutionRequest(execution: ExecutionRequestInit): Promise { let entrypoint = this.entrypoint; if (!entrypoint) { const { chainId, protocolVersion } = await this.pxe.getNodeInfo(); entrypoint = new DefaultEntrypoint(chainId, protocolVersion); } - return entrypoint.createTxExecutionRequest(executions); + return entrypoint.createTxExecutionRequest(execution); } getChainId(): Fr { diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index 7ee93045b6e..25493c9d5f6 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -2,8 +2,8 @@ import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; import { type AztecAddress, BatchCall, SignerlessWallet, type Wallet } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { type AztecNode } from '@aztec/circuit-types'; -import { DefaultMultiCallEntrypoint } from '@aztec/entrypoints/multi-call'; import { type DeployL1Contracts, type L1ContractAddresses, @@ -212,7 +212,7 @@ export async function createSandbox(config: Partial = {}) { if (config.enableGas) { await deployCanonicalL2GasToken( - new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint()), + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(aztecNodeConfig.chainId, aztecNodeConfig.version)), aztecNodeConfig.l1Contracts, ); } diff --git a/yarn-project/cli/src/cmds/create_account.ts b/yarn-project/cli/src/cmds/create_account.ts index 34a99f24e44..0b3ecbb3166 100644 --- a/yarn-project/cli/src/cmds/create_account.ts +++ b/yarn-project/cli/src/cmds/create_account.ts @@ -17,7 +17,7 @@ export async function createAccount( const account = getSchnorrAccount(client, actualPrivateKey, actualPrivateKey, Fr.ZERO); const { address, publicKey, partialAddress } = account.getCompleteAddress(); - const tx = await account.deploy(); + const tx = account.deploy(); const txHash = await tx.getTxHash(); debugLogger.verbose(`Account contract tx sent with hash ${txHash}`); if (wait) { diff --git a/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts new file mode 100644 index 00000000000..f5552190e01 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_account_init_fees.test.ts @@ -0,0 +1,337 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountManager, + type DebugLogger, + ExtendedNote, + Fr, + NativeFeePaymentMethod, + Note, + PrivateFeePaymentMethod, + PublicFeePaymentMethod, + Schnorr, + type TxHash, + TxStatus, + type Wallet, + computeMessageSecretHash, + generatePublicKey, +} from '@aztec/aztec.js'; +import { type AztecAddress, CompleteAddress, Fq, getContractClassFromArtifact } from '@aztec/circuits.js'; +import { + TokenContract as BananaCoin, + FPCContract, + type GasTokenContract, + SchnorrAccountContract, +} from '@aztec/noir-contracts.js'; + +import { jest } from '@jest/globals'; + +import { + type BalancesFn, + type EndToEndContext, + expectMapping, + getBalancesFn, + publicDeployAccounts, + setup, +} from './fixtures/utils.js'; +import { GasPortalTestingHarnessFactory, type IGasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; + +const TOKEN_NAME = 'BananaCoin'; +const TOKEN_SYMBOL = 'BC'; +const TOKEN_DECIMALS = 18n; +const BRIDGED_FPC_GAS = 444n; + +jest.setTimeout(1000_000); + +describe('e2e_fees_account_init', () => { + let ctx: EndToEndContext; + let logger: DebugLogger; + let sequencer: Wallet; + let sequencersAddress: AztecAddress; + let alice: Wallet; + + let gas: GasTokenContract; + let bananaCoin: BananaCoin; + let bananaFPC: FPCContract; + + let gasBridgeTestHarness: IGasBridgingTestHarness; + + let gasBalances: BalancesFn; + let bananaPublicBalances: BalancesFn; + let bananaPrivateBalances: BalancesFn; + + let bobsPrivateEncryptionKey: Fq; + let bobsPrivateSigningKey: Fq; + let bobsAccountManager: AccountManager; + let bobsAddress: AztecAddress; + + let bobsInitialGas: bigint; + let alicesInitialGas: bigint; + let sequencersInitialGas: bigint; + let fpcsInitialGas: bigint; + let fpcsInitialPublicBananas: bigint; + + let maxFee: bigint; + let actualFee: bigint; + + // run this after each test's setup phase to get the initial balances + async function initBalances() { + [[bobsInitialGas, alicesInitialGas, sequencersInitialGas, fpcsInitialGas], [fpcsInitialPublicBananas]] = + await Promise.all([ + gasBalances(bobsAddress, alice.getAddress(), sequencersAddress, bananaFPC.address), + bananaPublicBalances(bananaFPC.address), + ]); + } + + beforeAll(async () => { + ctx = await setup(2); + logger = ctx.logger; + [sequencer, alice] = ctx.wallets; + sequencersAddress = sequencer.getAddress(); + + await ctx.aztecNode.setConfig({ + allowedFeePaymentContractClasses: [getContractClassFromArtifact(FPCContract.artifact).id], + feeRecipient: sequencersAddress, + }); + + gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ + pxeService: ctx.pxe, + publicClient: ctx.deployL1ContractsValues.publicClient, + walletClient: ctx.deployL1ContractsValues.walletClient, + wallet: ctx.wallet, + logger: ctx.logger, + mockL1: false, + }); + + gas = gasBridgeTestHarness.l2Token; + + bananaCoin = await BananaCoin.deploy(sequencer, sequencersAddress, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS) + .send() + .deployed(); + + logger.verbose(`BananaCoin deployed at ${bananaCoin.address}`); + + bananaFPC = await FPCContract.deploy(sequencer, bananaCoin.address, gas.address).send().deployed(); + logger.verbose(`bananaPay deployed at ${bananaFPC.address}`); + await publicDeployAccounts(sequencer, [sequencer]); + + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, bananaFPC.address); + + bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, logger); + bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, logger); + gasBalances = getBalancesFn('⛽', gas.methods.balance_of_public, logger); + }); + + afterAll(() => ctx.teardown()); + + beforeEach(() => { + maxFee = 3n; + actualFee = 1n; + bobsPrivateEncryptionKey = Fq.random(); + bobsPrivateSigningKey = Fq.random(); + bobsAccountManager = getSchnorrAccount(ctx.pxe, bobsPrivateEncryptionKey, bobsPrivateSigningKey, Fr.random()); + bobsAddress = bobsAccountManager.getCompleteAddress().address; + }); + + describe('account pays its own fee', () => { + describe('in the gas token', () => { + beforeEach(async () => { + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, bobsAddress); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + await bobsAccountManager + .deploy({ + fee: { + maxFee, + paymentMethod: await NativeFeePaymentMethod.create(await bobsAccountManager.getWallet()), + }, + }) + .wait(); + + await expectMapping( + gasBalances, + [bobsAddress, sequencersAddress], + [bobsInitialGas - actualFee, sequencersInitialGas + actualFee], + ); + }); + }); + + describe('privately through an FPC', () => { + let mintedPrivateBananas: bigint; + beforeEach(async () => { + mintedPrivateBananas = 42n; + + // TODO the following sequence of events ends in a timeout + // 1. pxe.registerRecipient (aka just add the public key so pxe can encrypt notes) + // 2. mint note for mew account + // 3. accountManager.register (add pubkey + start a note processor) + // as a workaround, register (pubkey + note processors) the account first, before minting the note + await bobsAccountManager.register(); + + const secret = Fr.random(); + const secretHash = computeMessageSecretHash(secret); + const mintTx = await bananaCoin.methods.mint_private(mintedPrivateBananas, secretHash).send().wait(); + await addTransparentNoteToPxe(sequencersAddress, mintedPrivateBananas, secretHash, mintTx.txHash); + + // at this point, the new account owns a note + // but the account doesn't have a NoteProcessor registered + // so the note exists on the blockchain as an encrypted blob + // tell the pxe to start a note processor for the account ahead of its deployment + await bananaCoin.methods.redeem_shield(bobsAddress, mintedPrivateBananas, secret).send().wait(); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + const rebateSecret = Fr.random(); + const tx = await bobsAccountManager + .deploy({ + fee: { + maxFee, + paymentMethod: new PrivateFeePaymentMethod( + bananaCoin.address, + bananaFPC.address, + await bobsAccountManager.getWallet(), + rebateSecret, + ), + }, + }) + .wait(); + + expect(tx.status).toEqual(TxStatus.MINED); + + // the new account should have paid the full fee to the FPC + await expect(bananaPrivateBalances(bobsAddress)).resolves.toEqual([mintedPrivateBananas - maxFee]); + + // the FPC got paid through "unshield", so it's got a new public balance + await expect(bananaPublicBalances(bananaFPC.address)).resolves.toEqual([fpcsInitialPublicBananas + actualFee]); + + // the FPC should have paid the sequencer + await expect(gasBalances(bananaFPC.address, sequencersAddress)).resolves.toEqual([ + fpcsInitialGas - actualFee, + sequencersInitialGas + actualFee, + ]); + + // the new account should have received a refund + await expect( + // this rejects if note can't be added + addTransparentNoteToPxe(bobsAddress, maxFee - actualFee, computeMessageSecretHash(rebateSecret), tx.txHash), + ).resolves.toBeUndefined(); + + // and it can redeem the refund + await bananaCoin.methods + .redeem_shield(bobsAccountManager.getCompleteAddress().address, maxFee - actualFee, rebateSecret) + .send() + .wait(); + + await expect(bananaPrivateBalances(bobsAccountManager.getCompleteAddress().address)).resolves.toEqual([ + mintedPrivateBananas - actualFee, + ]); + }); + }); + + describe('public through an FPC', () => { + let mintedPublicBananas: bigint; + + beforeEach(async () => { + mintedPublicBananas = 37n; + await bananaCoin.methods.mint_public(bobsAddress, mintedPublicBananas).send().wait(); + }); + + beforeEach(initBalances); + + it('account pays for its own fee', async () => { + const tx = await bobsAccountManager + .deploy({ + skipPublicDeployment: false, + fee: { + maxFee, + paymentMethod: new PublicFeePaymentMethod( + bananaCoin.address, + bananaFPC.address, + await bobsAccountManager.getWallet(), + ), + }, + }) + .wait(); + + expect(tx.status).toEqual(TxStatus.MINED); + + // we should have paid the fee to the FPC + await expect( + bananaPublicBalances(bobsAccountManager.getCompleteAddress().address, bananaFPC.address), + ).resolves.toEqual([mintedPublicBananas - actualFee, fpcsInitialPublicBananas + actualFee]); + + // the FPC should have paid the sequencer + await expect(gasBalances(bananaFPC.address, sequencersAddress)).resolves.toEqual([ + fpcsInitialGas - actualFee, + sequencersInitialGas + actualFee, + ]); + }); + }); + }); + + describe('another account pays the fee', () => { + describe('in the gas token', () => { + beforeEach(async () => { + await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_FPC_GAS, BRIDGED_FPC_GAS, alice.getAddress()); + }); + + beforeEach(initBalances); + + it("alice pays for bob's account", async () => { + // bob generates the private keys for his account on his own + const instance = bobsAccountManager.getInstance(); + + // and gives the public keys to alice + const encPubKey = generatePublicKey(bobsPrivateEncryptionKey); + const signingPubKey = new Schnorr().computePublicKey(bobsPrivateSigningKey); + const completeAddress = CompleteAddress.fromPublicKeyAndInstance(encPubKey, instance); + + // alice registers the keys in the PXE + await ctx.pxe.registerRecipient(completeAddress); + + // and deploys bob's account, paying the fee from her balance + const tx = await SchnorrAccountContract.deployWithPublicKey(encPubKey, alice, signingPubKey.x, signingPubKey.y) + .send({ + contractAddressSalt: instance.salt, + skipClassRegistration: true, + skipPublicDeployment: true, + skipInitialization: false, + universalDeploy: true, + fee: { + maxFee, + paymentMethod: await NativeFeePaymentMethod.create(alice), + }, + }) + .wait(); + + expect(tx.status).toBe(TxStatus.MINED); + + await expectMapping( + gasBalances, + [alice.getAddress(), bobsAddress, sequencersAddress], + [alicesInitialGas - actualFee, bobsInitialGas, sequencersInitialGas + actualFee], + ); + + // bob can now use his wallet + const bobsWallet = await bobsAccountManager.getWallet(); + await expect(gas.withWallet(bobsWallet).methods.balance_of_public(alice.getAddress()).simulate()).resolves.toBe( + alicesInitialGas - actualFee, + ); + }); + }); + }); + + async function addTransparentNoteToPxe(owner: AztecAddress, amount: bigint, secretHash: Fr, txHash: TxHash) { + const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. + const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote + + const note = new Note([new Fr(amount), secretHash]); + // this note isn't encrypted but we need to provide a registered public key + const extendedNote = new ExtendedNote(note, owner, bananaCoin.address, storageSlot, noteTypeId, txHash); + await ctx.pxe.addNote(extendedNote); + } +}); diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 90348fa8d28..3c6406ae255 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -209,7 +209,7 @@ describe('e2e_dapp_subscription', () => { it('should call dapp subscription entrypoint', async () => { const dappPayload = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappPayload.createTxExecutionRequest([action]); + const txExReq = await dappPayload.createTxExecutionRequest({ calls: [action] }); const tx = await pxe.proveTx(txExReq, true); const sentTx = new SentTx(pxe, pxe.sendTx(tx)); await sentTx.wait(); @@ -263,7 +263,7 @@ describe('e2e_dapp_subscription', () => { async function dappIncrement() { const dappEntrypoint = new DefaultDappEntrypoint(aliceAddress, aliceWallet, subscriptionContract.address); const action = counterContract.methods.increment(bobAddress).request(); - const txExReq = await dappEntrypoint.createTxExecutionRequest([action]); + const txExReq = await dappEntrypoint.createTxExecutionRequest({ calls: [action] }); const tx = await pxe.proveTx(txExReq, true); const sentTx = new SentTx(pxe, pxe.sendTx(tx)); return sentTx.wait(); diff --git a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts index 7f70195b940..812350b4015 100644 --- a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts @@ -121,7 +121,7 @@ describe('e2e_p2p_network', () => { const submitTxsTo = async (pxe: PXEService, account: AztecAddress, numTxs: number) => { const txs: SentTx[] = []; for (let i = 0; i < numTxs; i++) { - const tx = await getSchnorrAccount(pxe, GrumpkinScalar.random(), GrumpkinScalar.random(), Fr.random()).deploy(); + const tx = getSchnorrAccount(pxe, GrumpkinScalar.random(), GrumpkinScalar.random(), Fr.random()).deploy(); logger.info(`Tx sent with hash ${await tx.getTxHash()}`); const receipt = await tx.getReceipt(); expect(receipt).toEqual( diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 39b4fba0df9..c4938e4376e 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -27,7 +27,7 @@ import { waitForPXE, } from '@aztec/aztec.js'; import { deployInstance, registerContractClass } from '@aztec/aztec.js/deployment'; -import { DefaultMultiCallEntrypoint } from '@aztec/entrypoints/multi-call'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { randomBytes } from '@aztec/foundation/crypto'; import { AvailabilityOracleAbi, @@ -260,9 +260,12 @@ async function setupWithRemoteEnvironment( const teardown = () => Promise.resolve(); if (['1', 'true'].includes(ENABLE_GAS)) { + const { chainId, protocolVersion } = await pxeClient.getNodeInfo(); // this contract might already have been deployed // the following function is idempotent - await deployCanonicalGasToken(new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint())); + await deployCanonicalGasToken( + new SignerlessWallet(pxeClient, new DefaultMultiCallEntrypoint(chainId, protocolVersion)), + ); } return { @@ -377,7 +380,9 @@ export async function setup( const { pxe, wallets } = await setupPXEService(numberOfAccounts, aztecNode!, pxeOpts, logger); if (['1', 'true'].includes(ENABLE_GAS)) { - await deployCanonicalGasToken(new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint())); + await deployCanonicalGasToken( + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(config.chainId, config.version)), + ); } const cheatCodes = CheatCodes.create(config.rpcUrl, pxe!); diff --git a/yarn-project/entrypoints/package.json b/yarn-project/entrypoints/package.json index 7e62b41ff6d..3ee6b4bbe95 100644 --- a/yarn-project/entrypoints/package.json +++ b/yarn-project/entrypoints/package.json @@ -6,8 +6,7 @@ "type": "module", "exports": { "./dapp": "./dest/dapp_entrypoint.js", - "./account": "./dest/account_entrypoint.js", - "./multi-call": "./dest/multi_call_entrypoint.js" + "./account": "./dest/account_entrypoint.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/entrypoints/src/account_entrypoint.ts b/yarn-project/entrypoints/src/account_entrypoint.ts index acc3c011e4e..8d222f71b99 100644 --- a/yarn-project/entrypoints/src/account_entrypoint.ts +++ b/yarn-project/entrypoints/src/account_entrypoint.ts @@ -1,11 +1,10 @@ import { type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface, type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; -import { type AztecAddress, FunctionData, GeneratorIndex, TxContext } from '@aztec/circuits.js'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type AztecAddress, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildAppPayload, buildFeePayload, hashPayload } from './entrypoint_payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -19,22 +18,23 @@ export class DefaultAccountEntrypoint implements EntrypointInterface { private version: number = DEFAULT_VERSION, ) {} - async createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise { - const { payload: appPayload, packedArguments: appPackedArguments } = buildAppPayload(executions); - const { payload: feePayload, packedArguments: feePackedArguments } = await buildFeePayload(feeOpts); + async createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls, fee } = exec; + const appPayload = EntrypointPayload.fromAppExecution(calls); + const feePayload = await EntrypointPayload.fromFeeOptions(fee); const abi = this.getEntrypointAbi(); const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [appPayload, feePayload])); - const appAuthWitness = await this.auth.createAuthWit(hashPayload(appPayload, GeneratorIndex.SIGNATURE_PAYLOAD)); - const feeAuthWitness = await this.auth.createAuthWit(hashPayload(feePayload, GeneratorIndex.FEE_PAYLOAD)); + const appAuthWitness = await this.auth.createAuthWit(appPayload.hash()); + const feeAuthWitness = await this.auth.createAuthWit(feePayload.hash()); const txRequest = TxExecutionRequest.from({ argsHash: entrypointPackedArgs.hash, origin: this.address, functionData: FunctionData.fromAbi(abi), txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...appPackedArguments, ...feePackedArguments, entrypointPackedArgs], + packedArguments: [...appPayload.packedArguments, ...feePayload.packedArguments, entrypointPackedArgs], authWitnesses: [appAuthWitness, feeAuthWitness], }); diff --git a/yarn-project/entrypoints/src/dapp_entrypoint.ts b/yarn-project/entrypoints/src/dapp_entrypoint.ts index 67e41819e68..f52593ba721 100644 --- a/yarn-project/entrypoints/src/dapp_entrypoint.ts +++ b/yarn-project/entrypoints/src/dapp_entrypoint.ts @@ -1,12 +1,11 @@ import { computeInnerAuthWitHash, computeOuterAuthWitHash } from '@aztec/aztec.js'; import { type AuthWitnessProvider } from '@aztec/aztec.js/account'; -import { type EntrypointInterface } from '@aztec/aztec.js/entrypoint'; -import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; +import { type EntrypointInterface, EntrypointPayload, type ExecutionRequestInit } from '@aztec/aztec.js/entrypoint'; +import { PackedArguments, TxExecutionRequest } from '@aztec/circuit-types'; import { type AztecAddress, Fr, FunctionData, TxContext } from '@aztec/circuits.js'; import { type FunctionAbi, encodeArguments } from '@aztec/foundation/abi'; import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js'; -import { buildDappPayload } from './entrypoint_payload.js'; /** * Implementation for an entrypoint interface that follows the default entrypoint signature @@ -21,11 +20,13 @@ export class DefaultDappEntrypoint implements EntrypointInterface { private version: number = DEFAULT_VERSION, ) {} - async createTxExecutionRequest(executions: FunctionCall[]): Promise { - if (executions.length !== 1) { - throw new Error('ILLEGAL'); + async createTxExecutionRequest(exec: ExecutionRequestInit): Promise { + const { calls } = exec; + if (calls.length !== 1) { + throw new Error(`Expected exactly 1 function call, got ${calls.length}`); } - const { payload, packedArguments } = buildDappPayload(executions[0]); + + const payload = EntrypointPayload.fromFunctionCalls(calls); const abi = this.getEntrypointAbi(); const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [payload, this.userAddress])); @@ -47,7 +48,7 @@ export class DefaultDappEntrypoint implements EntrypointInterface { origin: this.dappEntrypointAddress, functionData, txContext: TxContext.empty(this.chainId, this.version), - packedArguments: [...packedArguments, entrypointPackedArgs], + packedArguments: [...payload.packedArguments, entrypointPackedArgs], authWitnesses: [authWitness], }); diff --git a/yarn-project/entrypoints/src/entrypoint_payload.ts b/yarn-project/entrypoints/src/entrypoint_payload.ts deleted file mode 100644 index 894dcdc4f5e..00000000000 --- a/yarn-project/entrypoints/src/entrypoint_payload.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { type FeeOptions } from '@aztec/aztec.js/entrypoint'; -import { Fr } from '@aztec/aztec.js/fields'; -import { type FunctionCall, PackedArguments, emptyFunctionCall } from '@aztec/circuit-types'; -import { type AztecAddress } from '@aztec/circuits.js'; -import { padArrayEnd } from '@aztec/foundation/collection'; -import { pedersenHash } from '@aztec/foundation/crypto'; - -// These must match the values defined in: -// - noir-projects/aztec-nr/aztec/src/entrypoint/app.nr -const ACCOUNT_MAX_CALLS = 4; -// - and noir-projects/aztec-nr/aztec/src/entrypoint/fee.nr -const FEE_MAX_CALLS = 2; - -/** Encoded function call for account contract entrypoint */ -type EntrypointFunctionCall = { - // eslint-disable-next-line camelcase - /** Arguments hash for the call */ - args_hash: Fr; - // eslint-disable-next-line camelcase - /** Selector of the function to call */ - function_selector: Fr; - // eslint-disable-next-line camelcase - /** Address of the contract to call */ - target_address: Fr; - // eslint-disable-next-line camelcase - /** Whether the function is public or private */ - is_public: boolean; -}; - -/** Encoded payload for the account contract entrypoint */ -type EntrypointPayload = { - // eslint-disable-next-line camelcase - /** Encoded function calls to execute */ - function_calls: EntrypointFunctionCall[]; - /** A nonce for replay protection */ - nonce: Fr; -}; - -/** Represents a generic payload to be executed in the context of an account contract */ -export type PayloadWithArguments = { - /** The payload to be run */ - payload: EntrypointPayload; - /** The packed arguments for the function calls */ - packedArguments: PackedArguments[]; -}; - -/** - * Builds a payload to be sent to the account contract - * @param calls - The function calls to run - * @param maxCalls - The maximum number of call expected to be run. Used for padding - * @returns A payload object and packed arguments - */ -function buildPayload(calls: FunctionCall[], maxCalls: number): PayloadWithArguments { - const nonce = Fr.random(); - - const paddedCalls = padArrayEnd(calls, emptyFunctionCall(), maxCalls); - const packedArguments: PackedArguments[] = []; - for (const call of paddedCalls) { - packedArguments.push(PackedArguments.fromArgs(call.args)); - } - - const formattedCalls: EntrypointFunctionCall[] = paddedCalls.map((call, index) => ({ - // eslint-disable-next-line camelcase - args_hash: packedArguments[index].hash, - // eslint-disable-next-line camelcase - function_selector: call.functionData.selector.toField(), - // eslint-disable-next-line camelcase - target_address: call.to.toField(), - // eslint-disable-next-line camelcase - is_public: !call.functionData.isPrivate, - })); - - return { - payload: { - // eslint-disable-next-line camelcase - function_calls: formattedCalls, - nonce, - }, - packedArguments, - }; -} - -/** builds the payload for a Dapp entrypoint */ -export function buildDappPayload(call: FunctionCall): PayloadWithArguments { - return buildPayload([call], 1); -} - -/** Assembles an entrypoint app payload from a set of private and public function calls */ -export function buildAppPayload(calls: FunctionCall[]): PayloadWithArguments { - return buildPayload(calls, ACCOUNT_MAX_CALLS); -} - -/** Creates the payload for paying the fee for a transaction */ -export async function buildFeePayload(feeOpts?: FeeOptions): Promise { - const calls = feeOpts ? await feeOpts.paymentMethod.getFunctionCalls(new Fr(feeOpts.maxFee)) : []; - return buildPayload(calls, FEE_MAX_CALLS); -} - -// TODO (dogfooding) change all of these names app/dapp/fee/payload and generator indices for all of them -/** Hashes a payload to a 32-byte buffer */ -export function hashPayload(payload: EntrypointPayload, generatorIndex: number) { - return pedersenHash(flattenPayload(payload), generatorIndex); -} - -/** Hash the payload for a dapp */ -export function hashDappPayload(payload: EntrypointPayload, userAddress: AztecAddress, generatorIndex: number) { - return pedersenHash([...flattenPayload(payload), userAddress], generatorIndex); -} - -/** Flattens an payload */ -function flattenPayload(payload: EntrypointPayload) { - return [ - ...payload.function_calls.flatMap(call => [ - call.args_hash, - call.function_selector, - call.target_address, - new Fr(call.is_public), - ]), - payload.nonce, - ]; -} diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts index aaa02291c71..1a9fb2b8901 100644 --- a/yarn-project/foundation/src/abi/abi.ts +++ b/yarn-project/foundation/src/abi/abi.ts @@ -383,27 +383,27 @@ export function getDefaultInitializer(contractArtifact: ContractArtifact): Funct /** * Returns an initializer from the contract. - * @param initalizerNameOrArtifact - The name of the constructor, or the artifact of the constructor, or undefined + * @param initializerNameOrArtifact - The name of the constructor, or the artifact of the constructor, or undefined * to pick the default initializer. */ export function getInitializer( contract: ContractArtifact, - initalizerNameOrArtifact: string | undefined | FunctionArtifact, + initializerNameOrArtifact: string | undefined | FunctionArtifact, ): FunctionArtifact | undefined { - if (typeof initalizerNameOrArtifact === 'string') { - const found = contract.functions.find(f => f.name === initalizerNameOrArtifact); + if (typeof initializerNameOrArtifact === 'string') { + const found = contract.functions.find(f => f.name === initializerNameOrArtifact); if (!found) { - throw new Error(`Constructor method ${initalizerNameOrArtifact} not found in contract artifact`); + throw new Error(`Constructor method ${initializerNameOrArtifact} not found in contract artifact`); } else if (!found.isInitializer) { - throw new Error(`Method ${initalizerNameOrArtifact} is not an initializer`); + throw new Error(`Method ${initializerNameOrArtifact} is not an initializer`); } return found; - } else if (initalizerNameOrArtifact === undefined) { + } else if (initializerNameOrArtifact === undefined) { return getDefaultInitializer(contract); } else { - if (!initalizerNameOrArtifact.isInitializer) { - throw new Error(`Method ${initalizerNameOrArtifact.name} is not an initializer`); + if (!initializerNameOrArtifact.isInitializer) { + throw new Error(`Method ${initializerNameOrArtifact.name} is not an initializer`); } - return initalizerNameOrArtifact; + return initializerNameOrArtifact; } }