Skip to content

Commit

Permalink
feat: add fee payment methods (#4504)
Browse files Browse the repository at this point in the history
This PR updates the account entrypoints to add support for fee payments.
The concept of a fee payment method has been added, with two examples: a
native token payment method (where the caller pays in the native gas
token) and a generic payment method (where the sender can use a fee
preparation contract).
  • Loading branch information
alexghr authored Feb 12, 2024
1 parent da2f5ed commit d107746
Show file tree
Hide file tree
Showing 27 changed files with 523 additions and 214 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ You will learn:

- How to write a custom account contract in Aztec.nr
- The entrypoint function for transaction authentication and call execution
- The AccountActions module and EntrypointPayload struct, necessary inclusions for any account contract
- The AccountActions module and entrypoint payload structs, necessary inclusions for any account contract
- Customizing authorization validation within the `is_valid` function (using Schnorr signatures as an example)
- Typescript glue code to format and authenticate transactions
- Deploying and testing the account contract
Expand Down Expand Up @@ -43,14 +43,16 @@ Public Key: 0x0ede151adaef1cfcc1b3e152ea39f00c5cda3f3857cef00decb049d283672dc71

:::

The important part of this contract is the `entrypoint` function, which will be the first function executed in any transaction originated from this account. This function has two main responsibilities: authenticating the transaction and executing calls. It receives a `payload` with the list of function calls to execute, and requests a corresponding auth witness from an oracle to validate it. You will find this logic implemented in the `AccountActions` module, which uses the `EntrypointPayload` struct:
The important part of this contract is the `entrypoint` function, which will be the first function executed in any transaction originated from this account. This function has two main responsibilities: authenticating the transaction and executing calls. It receives a `payload` with the list of function calls to execute, and requests a corresponding auth witness from an oracle to validate it. You will find this logic implemented in the `AccountActions` module, which use the `AppPayload` and `FeePayload` structs:

#include_code entrypoint yarn-project/aztec-nr/authwit/src/account.nr rust

#include_code entrypoint-struct yarn-project/aztec-nr/authwit/src/entrypoint.nr rust
#include_code app-payload-struct yarn-project/aztec-nr/authwit/src/entrypoint/app.nr rust

#include_code fee-payload-struct yarn-project/aztec-nr/authwit/src/entrypoint/fee.nr rust

:::info
Using the `AccountActions` module and the `EntrypointPayload` struct is not mandatory. You can package the instructions to be carried out by your account contract however you want. However, using these modules can save you a lot of time when writing a new account contract, both in Noir and in Typescript.
Using the `AccountActions` module and the payload structs is not mandatory. You can package the instructions to be carried out by your account contract however you want. However, using these modules can save you a lot of time when writing a new account contract, both in Noir and in Typescript.
:::

The `AccountActions` module provides default implementations for most of the account contract methods needed, but it requires a function for validating an auth witness. In this function you will customize how your account validates an action: whether it is using a specific signature scheme, a multi-party approval, a password, etc.
Expand Down Expand Up @@ -100,5 +102,3 @@ To make sure that we are actually validating the provided signature in our accou
#include_code account-contract-fails yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts typescript

Lo and behold, we get `Error: Assertion failed: 'verification == true'` when running the snippet above, pointing to the line in our account contract where we verify the Schnorr signature.


113 changes: 66 additions & 47 deletions yarn-project/accounts/src/defaults/account_entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { AuthWitnessProvider, EntrypointInterface } from '@aztec/aztec.js/account';
import { AuthWitnessProvider, EntrypointInterface, FeeOptions } from '@aztec/aztec.js/account';
import { FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types';
import { AztecAddress, Fr, FunctionData, TxContext } from '@aztec/circuits.js';
import { AztecAddress, Fr, FunctionData, GeneratorIndex, TxContext } from '@aztec/circuits.js';
import { FunctionAbi, encodeArguments } from '@aztec/foundation/abi';

import { DEFAULT_CHAIN_ID, DEFAULT_VERSION } from './constants.js';
import { buildPayload, hashPayload } from './entrypoint_payload.js';
import { buildAppPayload, buildFeePayload, hashPayload } from './entrypoint_payload.js';

/**
* Implementation for an entrypoint interface that follows the default entrypoint signature
* for an account, which accepts an EntrypointPayload as defined in noir-libs/aztec-noir/src/entrypoint.nr.
* for an account, which accepts an AppPayload and a FeePayload as defined in noir-libs/aztec-noir/src/entrypoint module
*/
export class DefaultAccountEntrypoint implements EntrypointInterface {
constructor(
Expand All @@ -18,19 +18,27 @@ export class DefaultAccountEntrypoint implements EntrypointInterface {
private version: number = DEFAULT_VERSION,
) {}

async createTxExecutionRequest(executions: FunctionCall[]): Promise<TxExecutionRequest> {
const { payload, packedArguments: callsPackedArguments } = buildPayload(executions);
async createTxExecutionRequest(executions: FunctionCall[], feeOpts?: FeeOptions): Promise<TxExecutionRequest> {
const { payload: appPayload, packedArguments: appPackedArguments } = buildAppPayload(executions);
const { payload: feePayload, packedArguments: feePackedArguments } = buildFeePayload(feeOpts);

const abi = this.getEntrypointAbi();
const packedArgs = PackedArguments.fromArgs(encodeArguments(abi, [payload]));
const message = Fr.fromBuffer(hashPayload(payload));
const authWitness = await this.auth.createAuthWitness(message);
const entrypointPackedArgs = PackedArguments.fromArgs(encodeArguments(abi, [appPayload, feePayload]));

const appAuthWitness = await this.auth.createAuthWitness(
Fr.fromBuffer(hashPayload(appPayload, GeneratorIndex.SIGNATURE_PAYLOAD)),
);
const feeAuthWitness = await this.auth.createAuthWitness(
Fr.fromBuffer(hashPayload(feePayload, GeneratorIndex.FEE_PAYLOAD)),
);

const txRequest = TxExecutionRequest.from({
argsHash: packedArgs.hash,
argsHash: entrypointPackedArgs.hash,
origin: this.address,
functionData: FunctionData.fromAbi(abi),
txContext: TxContext.empty(this.chainId, this.version),
packedArguments: [...callsPackedArguments, packedArgs],
authWitnesses: [authWitness],
packedArguments: [...appPackedArguments, ...feePackedArguments, entrypointPackedArgs],
authWitnesses: [appAuthWitness, feeAuthWitness],
});

return txRequest;
Expand All @@ -43,10 +51,10 @@ export class DefaultAccountEntrypoint implements EntrypointInterface {
isInternal: false,
parameters: [
{
name: 'payload',
name: 'app_payload',
type: {
kind: 'struct',
path: 'authwit::entrypoint::EntrypointPayload',
path: 'authwit::entrypoint::app::AppPayload',
fields: [
{
name: 'function_calls',
Expand All @@ -55,62 +63,73 @@ export class DefaultAccountEntrypoint implements EntrypointInterface {
length: 4,
type: {
kind: 'struct',
path: 'authwit::entrypoint::FunctionCall',
path: 'authwit::entrypoint::function_call::FunctionCall',
fields: [
{ name: 'args_hash', type: { kind: 'field' } },
{
name: 'args_hash',
name: 'function_selector',
type: {
kind: 'field',
kind: 'struct',
path: 'authwit::aztec::protocol_types::abis::function_selector::FunctionSelector',
fields: [{ name: 'inner', type: { kind: 'integer', sign: 'unsigned', width: 32 } }],
},
},
{
name: 'function_selector',
name: 'target_address',
type: {
kind: 'struct',
path: 'aztec::protocol_types::abis::function_selector::FunctionSelector',
fields: [
{
name: 'inner',
type: {
kind: 'integer',
sign: 'unsigned',
width: 32,
},
},
],
path: 'authwit::aztec::protocol_types::address::AztecAddress',
fields: [{ name: 'inner', type: { kind: 'field' } }],
},
},
{ name: 'is_public', type: { kind: 'boolean' } },
],
},
},
},
{ name: 'nonce', type: { kind: 'field' } },
],
},
visibility: 'public',
},
{
name: 'fee_payload',
type: {
kind: 'struct',
path: 'authwit::entrypoint::fee::FeePayload',
fields: [
{
name: 'function_calls',
type: {
kind: 'array',
length: 2,
type: {
kind: 'struct',
path: 'authwit::entrypoint::function_call::FunctionCall',
fields: [
{ name: 'args_hash', type: { kind: 'field' } },
{
name: 'target_address',
name: 'function_selector',
type: {
kind: 'struct',
path: 'aztec::protocol_types::address::AztecAddress',
fields: [
{
name: 'inner',
type: {
kind: 'field',
},
},
],
path: 'authwit::aztec::protocol_types::abis::function_selector::FunctionSelector',
fields: [{ name: 'inner', type: { kind: 'integer', sign: 'unsigned', width: 32 } }],
},
},
{
name: 'is_public',
name: 'target_address',
type: {
kind: 'boolean',
kind: 'struct',
path: 'authwit::aztec::protocol_types::address::AztecAddress',
fields: [{ name: 'inner', type: { kind: 'field' } }],
},
},
{ name: 'is_public', type: { kind: 'boolean' } },
],
},
},
},
{
name: 'nonce',
type: {
kind: 'field',
},
},
{ name: 'nonce', type: { kind: 'field' } },
],
},
visibility: 'public',
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,4 +1,4 @@
import { AccountInterface, AuthWitnessProvider, EntrypointInterface } from '@aztec/aztec.js/account';
import { AccountInterface, AuthWitnessProvider, EntrypointInterface, FeeOptions } from '@aztec/aztec.js/account';
import { AuthWitness, FunctionCall, TxExecutionRequest } from '@aztec/circuit-types';
import { CompleteAddress, Fr } from '@aztec/circuits.js';
import { NodeInfo } from '@aztec/types/interfaces';
Expand All @@ -7,7 +7,7 @@ import { DefaultAccountEntrypoint } from './account_entrypoint.js';

/**
* Default implementation for an account interface. Requires that the account uses the default
* entrypoint signature, which accepts an EntrypointPayload as defined in noir-libs/aztec-noir/src/entrypoint.nr.
* entrypoint signature, which accept an AppPayload and a FeePayload as defined in noir-libs/aztec-noir/src/entrypoint module
*/
export class DefaultAccountInterface implements AccountInterface {
private entrypoint: EntrypointInterface;
Expand All @@ -25,8 +25,8 @@ export class DefaultAccountInterface implements AccountInterface {
);
}

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

createAuthWitness(message: Fr): Promise<AuthWitness> {
Expand Down
49 changes: 36 additions & 13 deletions yarn-project/accounts/src/defaults/entrypoint_payload.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { FeeOptions } from '@aztec/aztec.js/account';
import { Fr } from '@aztec/aztec.js/fields';
import { 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';

// These must match the values defined in yarn-project/aztec-nr/aztec/src/entrypoint.nr
// These must match the values defined in:
// - yarn-project/aztec-nr/aztec/src/entrypoint/app.nr
const ACCOUNT_MAX_CALLS = 4;
// - and yarn-project/aztec-nr/aztec/src/entrypoint/fee.nr
const FEE_MAX_CALLS = 2;

/** Encoded function call for account contract entrypoint */
type EntrypointFunctionCall = {
Expand All @@ -23,24 +27,32 @@ type EntrypointFunctionCall = {
};

/** Encoded payload for the account contract entrypoint */
export type EntrypointPayload = {
type EntrypointPayload = {
// eslint-disable-next-line camelcase
/** Encoded function calls to execute */
function_calls: EntrypointFunctionCall[];
/** A nonce for replay protection */
nonce: Fr;
};

/** Assembles an entrypoint payload from a set of private and public function calls */
export function buildPayload(calls: FunctionCall[]): {
/** The payload for the entrypoint function */
/** 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 of functions called */
/** 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(), ACCOUNT_MAX_CALLS);
const paddedCalls = padArrayEnd(calls, emptyFunctionCall(), maxCalls);
const packedArguments: PackedArguments[] = [];
for (const call of paddedCalls) {
packedArguments.push(PackedArguments.fromArgs(call.args));
Expand All @@ -67,15 +79,26 @@ export function buildPayload(calls: FunctionCall[]): {
};
}

/** Hashes an entrypoint payload to a 32-byte buffer (useful for signing) */
export function hashPayload(payload: EntrypointPayload) {
/** 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 function buildFeePayload(feeOpts?: FeeOptions): PayloadWithArguments {
const calls = feeOpts?.paymentMethod.getFunctionCalls(new Fr(feeOpts.maxFee)) ?? [];
return buildPayload(calls, FEE_MAX_CALLS);
}

/** Hashes a payload to a 32-byte buffer */
export function hashPayload(payload: EntrypointPayload, generatorIndex: number) {
return pedersenHash(
flattenPayload(payload).map(fr => fr.toBuffer()),
GeneratorIndex.SIGNATURE_PAYLOAD,
generatorIndex,
);
}

/** Flattens an entrypoint payload */
/** Flattens an payload */
function flattenPayload(payload: EntrypointPayload) {
return [
...payload.function_calls.flatMap(call => [
Expand Down
18 changes: 12 additions & 6 deletions yarn-project/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use dep::aztec::context::{PrivateContext, PublicContext, Context};
use dep::aztec::state_vars::{map::Map, public_state::PublicState};

use crate::entrypoint::EntrypointPayload;
use crate::entrypoint::{app::AppPayload, fee::FeePayload};
use crate::auth::IS_VALID_SELECTOR;

struct AccountActions {
Expand Down Expand Up @@ -54,12 +54,18 @@ impl AccountActions {
}

// docs:start:entrypoint
pub fn entrypoint(self, payload: EntrypointPayload) {
let message_hash = payload.hash();
pub fn entrypoint(self, app_payload: AppPayload, fee_payload: FeePayload) {
let valid_fn = self.is_valid_impl;
let private_context = self.context.private.unwrap();
assert(valid_fn(private_context, message_hash));
payload.execute_calls(private_context);
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_max_non_revertible_side_effect_counter();

let app_hash = app_payload.hash();
assert(valid_fn(private_context, app_hash));
app_payload.execute_calls(private_context);
}
// docs:end:entrypoint

Expand Down
Loading

0 comments on commit d107746

Please sign in to comment.