Skip to content

Commit

Permalink
feat!: pay fee for account init (#5601)
Browse files Browse the repository at this point in the history
This PR enables accounts to pay tx fees when they're deployed. To
achieve this a new deployment method was added that's used by the
`AccountManager` class to optionally register/publicly deploy and
initialize the target account contract.

Entrypoint classes now accept authwits/packed arguments alongside the
normal function calls from before. This is needed so that authwits could
be created in a parent context and then passed along as transient
authwits to the transaction (see `ExecutionRequestInit` wrapper type)

Initializing an account contract can use any of the three existing
payment methods:
- using bridged gas token from L1
- paying privately through a fee payment contract
- paying publicly through a fee payment contract

In order to use fee payment contracts this PR adds `noinitcheck` to
`spend_private_authwit` and `spend_public_authwit` because it's not
possible to read the init nullifier in the current tx (protocol
limitation). Instead the contract relies on the note containing the
public key to exist to validate that the contract has been initialized
correctly.

An extra payment flow is tested as well: a third party takes the
account's public keys and deploys and initializes the account while
paying the associated fee. This simulates the flow where a deployment
service is used that takes payment through a side chain (e.g. fiat).

Breaking change: moved `DefaultMultiCallEntrypoint` to aztec.js

This PR supersedes #5540 and #5543.

Fix #5190 #5191 #5544.
  • Loading branch information
alexghr authored Apr 10, 2024
1 parent 80c498d commit aca804f
Show file tree
Hide file tree
Showing 33 changed files with 834 additions and 262 deletions.
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1546,6 +1557,7 @@ workflows:
- e2e-card-game
- e2e-avm-simulator
- e2e-fees
- e2e-account-init-fees
- e2e-dapp-subscription
- pxe
- boxes-vanilla
Expand Down
10 changes: 10 additions & 0 deletions noir-projects/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
15 changes: 15 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -58,6 +71,7 @@ contract SchnorrAccount {
}

#[aztec(public)]
#[aztec(noinitcheck)]
fn spend_public_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::public(
&mut context,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/accounts/src/defaults/account_interface.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,8 +29,8 @@ export class DefaultAccountInterface implements AccountInterface {
this.version = new Fr(nodeInfo.protocolVersion);
}

createTxExecutionRequest(executions: FunctionCall[], fee?: FeeOptions): Promise<TxExecutionRequest> {
return this.entrypoint.createTxExecutionRequest(executions, fee);
createTxExecutionRequest(execution: ExecutionRequestInit): Promise<TxExecutionRequest> {
return this.entrypoint.createTxExecutionRequest(execution);
}

createAuthWit(messageHash: Fr): Promise<AuthWitness> {
Expand Down
8 changes: 7 additions & 1 deletion yarn-project/aztec.js/src/account/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand All @@ -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
71 changes: 71 additions & 0 deletions yarn-project/aztec.js/src/account_manager/deploy_account_method.ts
Original file line number Diff line number Diff line change
@@ -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<ExecutionRequestInit> {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,8 +15,8 @@ export type DeployAccountTxReceipt = FieldsOf<TxReceipt> & {
* 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<TxHash>) {
super(wallet, txHashPromise);
constructor(pxe: PXE, txHashPromise: Promise<TxHash>, private getWalletPromise: Promise<Wallet>) {
super(pxe, txHashPromise);
}

/**
Expand All @@ -36,7 +36,8 @@ export class DeployAccountSentTx extends SentTx {
*/
public async wait(opts: WaitOpts = DefaultWaitOpts): Promise<DeployAccountTxReceipt> {
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 };
}
}
Loading

0 comments on commit aca804f

Please sign in to comment.