Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [aztec-nr / Aztec.js] Account entrypoint authorization now binds the fee-payment method and cancellation flag

The account entrypoint (`AccountActions::entrypoint`) previously authorized only the app payload hash. It now
authorizes a combined hash over the app payload, the `fee_payment_method` selector, and the `cancellable` flag, so
the account's approval covers the fee-payer and cancellation side effects rather than the call list alone. This
prevents a party holding an account's authorization witness (for example a relayer or delegated prover) from
reusing it while switching the fee-payment mode or cancellation flag.

This changes the authorized message preimage, so it is a breaking change:

- **In-repo account contracts** all delegate to `AccountActions::entrypoint`, so they pick up the change on
recompilation with no source edits. Recompile and redeploy account contracts against the new library.
- **Old deployed account bytecode** does not authenticate against witnesses produced by the updated client, and
the updated bytecode does not accept witnesses produced by an old client. Client and account bytecode must be
upgraded together.
- **Third-party wallets or tooling** that build the account entrypoint authorization witness themselves (mirroring
`DefaultAccountEntrypoint`) must include the fee-payment method and cancellation flag in the witnessed hash,
using the new `entrypoint_payload` domain separator, instead of hashing the encoded calls alone.

Gas settings are not yet bound; that is planned as a follow-up.

### [Aztec.js] A function's return type is a single `returnType`, not a `returnTypes` list

`FunctionAbi.returnTypes` is deprecated in favor of the single optional `FunctionAbi.returnType`, since multiple return values are already expressed as one `tuple` type. Read it through `getFunctionReturnType(abi)`, which also resolves artifacts serialized before `returnType` existed. `FunctionCall.returnTypes` is likewise replaced by `FunctionCall.returnType`.
Expand Down
28 changes: 26 additions & 2 deletions noir-projects/labs/aztec-nr/aztec/src/authwit/account.nr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ use crate::authwit::entrypoint::app::AppPayload;
/// Used to produce cancellable (replaceable) transactions.
pub(crate) global DOM_SEP__TX_NULLIFIER: u32 = 1025801951;

/// Domain separator for the account entrypoint payload authorization message.
///
/// Binds the fee-payment selector and cancellation flag alongside the app payload so that the account authorizes
/// them together.
pub(crate) global DOM_SEP__ENTRYPOINT_PAYLOAD: u32 = 3045079954;

/// Computes the inner hash the account authorizes when invoked through its entrypoint.
///
/// Binds the app payload together with the fee-payment selector and cancellation flag. Both drive fee-payer and
/// cancellation side effects, so authorizing the app payload alone would let a party holding the witness switch the
/// fee-payment mode or cancellation flag while keeping the account's approval valid. The gas settings are not yet
/// bound: doing so requires the self-paid-deploy flow to build this witness with the settings resolved during send
/// preparation.
fn compute_entrypoint_payload_hash(app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) -> Field {
poseidon2_hash_with_separator(
[app_payload.hash(), fee_payment_method as Field, cancellable as Field],
DOM_SEP__ENTRYPOINT_PAYLOAD,
)
}

pub struct AccountActions<Context> {
context: Context,
is_valid_impl: fn(&mut PrivateContext, Field) -> bool,
Expand Down Expand Up @@ -39,7 +59,10 @@ pub global AccountFeePaymentMethodOptions: AccountFeePaymentMethodOptionsEnum =
/// Implements logic to verify authorization and execute payloads.
impl AccountActions<&mut PrivateContext> {

/// Verifies that the `app_hash` is authorized and executes the `app_payload`.
/// Verifies that the entrypoint payload is authorized and executes the `app_payload`.
///
/// The authorized message binds the app payload together with `fee_payment_method` and `cancellable`, so the
/// account's approval covers the fee-payer and cancellation side effects rather than the call list alone.
///
/// @param app_payload The payload that contains the calls to be executed in the app phase.
///
Expand All @@ -60,11 +83,12 @@ impl AccountActions<&mut PrivateContext> {
pub fn entrypoint(self, app_payload: AppPayload, fee_payment_method: u8, cancellable: bool) {
let valid_fn = self.is_valid_impl;

let inner_hash = compute_entrypoint_payload_hash(app_payload, fee_payment_method, cancellable);
let message_hash = compute_authwit_message_hash(
self.context.this_address(),
self.context.chain_id(),
self.context.version(),
app_payload.hash(),
inner_hash,
);
assert(valid_fn(self.context, message_hash));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! that the whole set (every aztec-nr separator plus every protocol separator) is collision-free. The protocol enforces
//! the same for its own separators in its own tests.

use crate::authwit::account::DOM_SEP__TX_NULLIFIER;
use crate::authwit::account::{DOM_SEP__ENTRYPOINT_PAYLOAD, DOM_SEP__TX_NULLIFIER};
use crate::authwit::auth::DOM_SEP__AUTHWIT_NULLIFIER;
use crate::keys::ecdh_shared_secret::{DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY};
use crate::macros::functions::initialization_utils::DOM_SEP__INITIALIZATION_NULLIFIER;
Expand Down Expand Up @@ -123,6 +123,7 @@ unconstrained fn domain_separators_are_valid() {
),
);
all = all.push_back(derived(DOM_SEP__TX_NULLIFIER, "tx_nullifier"));
all = all.push_back(derived(DOM_SEP__ENTRYPOINT_PAYLOAD, "entrypoint_payload"));
all = all.push_back(derived(DOM_SEP__AUTHWIT_NULLIFIER, "authwit_nullifier"));
all = all.push_back(derived(DOM_SEP__ECDH_SUBKEY, "ecdh_subkey"));
all = all.push_back(derived(DOM_SEP__ECDH_FIELD_MASK, "ecdh_field_mask"));
Expand Down
84 changes: 84 additions & 0 deletions yarn-project/entrypoints/src/account_entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { poseidon2HashBytes } from '@aztec/foundation/crypto/poseidon';
import { Fr } from '@aztec/foundation/curves/bn254';
import { AuthWitness } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { GasSettings } from '@aztec/stdlib/gas';
import { ExecutionPayload } from '@aztec/stdlib/tx';

import {
AccountFeePaymentMethodOptions,
DefaultAccountEntrypoint,
type DefaultAccountEntrypointOptions,
ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR,
} from './account_entrypoint.js';
import type { AuthWitnessProvider, ChainInfo } from './interfaces.js';

describe('DefaultAccountEntrypoint', () => {
// Returns the message hash as the witness request hash, so tests can observe exactly what the account is asked
// to authorize without involving keys.
const authWitnessProvider: AuthWitnessProvider = {
createAuthWit: (messageHash: Fr | Buffer) =>
Promise.resolve(
new AuthWitness(Fr.fromBuffer(Buffer.isBuffer(messageHash) ? messageHash : messageHash.toBuffer()), []),
),
};

const address = AztecAddress.fromNumberUnsafe(42);
const chainInfo: ChainInfo = { chainId: new Fr(1), version: new Fr(2) };

const gasSettings = GasSettings.from({
gasLimits: { daGas: 100, l2Gas: 200 },
teardownGasLimits: { daGas: 10, l2Gas: 20 },
maxFeesPerGas: { feePerDaGas: 3n, feePerL2Gas: 4n },
maxPriorityFeesPerGas: { feePerDaGas: 1n, feePerL2Gas: 2n },
});

const baseOptions: DefaultAccountEntrypointOptions = {
txNonce: new Fr(7),
cancellable: false,
feePaymentMethodOptions: AccountFeePaymentMethodOptions.EXTERNAL,
};

const getPayloadAuthWitnessHash = async (options: DefaultAccountEntrypointOptions): Promise<Fr> => {
const entrypoint = new DefaultAccountEntrypoint(address, authWitnessProvider);
const request = await entrypoint.createTxExecutionRequest(
ExecutionPayload.empty(),
gasSettings,
chainInfo,
options,
);
return request.authWitnesses.at(-1)!.requestHash;
};

it('computes the same payload auth witness for identical requests', async () => {
const first = await getPayloadAuthWitnessHash(baseOptions);
const second = await getPayloadAuthWitnessHash(baseOptions);
expect(first.equals(second)).toBe(true);
});

it.each([
[AccountFeePaymentMethodOptions.EXTERNAL, AccountFeePaymentMethodOptions.PREEXISTING_FEE_JUICE],
[AccountFeePaymentMethodOptions.EXTERNAL, AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM],
[AccountFeePaymentMethodOptions.PREEXISTING_FEE_JUICE, AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM],
])('binds the fee payment method into the payload auth witness (%i vs %i)', async (from, to) => {
const first = await getPayloadAuthWitnessHash({ ...baseOptions, feePaymentMethodOptions: from });
const second = await getPayloadAuthWitnessHash({ ...baseOptions, feePaymentMethodOptions: to });
expect(first.equals(second)).toBe(false);
});

it('binds the cancellable flag into the payload auth witness', async () => {
const first = await getPayloadAuthWitnessHash({ ...baseOptions, cancellable: false });
const second = await getPayloadAuthWitnessHash({ ...baseOptions, cancellable: true });
expect(first.equals(second)).toBe(false);
});

// Guards against drift from the Noir DOM_SEP__ENTRYPOINT_PAYLOAD, which is hand-mirrored here. Re-derives the
// value from the separator name the same way the Noir domain separators are derived (poseidon over the
// "az_dom_sep__<name>" byte string, truncated to a u32) rather than pinning a magic number.
it('mirrors the Noir entrypoint payload domain separator', async () => {
const derived = Number(
(await poseidon2HashBytes(Buffer.from('az_dom_sep__entrypoint_payload'))).toBigInt() & 0xffffffffn,
);
expect(ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR).toEqual(derived);
});
});
27 changes: 26 additions & 1 deletion yarn-project/entrypoints/src/account_entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon';
import { Fr } from '@aztec/foundation/curves/bn254';
import {
type FunctionAbi,
Expand All @@ -14,6 +15,30 @@ import { ExecutionPayload, HashedValues, TxContext, TxExecutionRequest } from '@
import { EncodedAppEntrypointCalls } from './encoding.js';
import type { AuthWitnessProvider, ChainInfo, EntrypointInterface } from './interfaces.js';

/**
* Domain separator for the account entrypoint payload authorization message. Mirrors DOM_SEP__ENTRYPOINT_PAYLOAD
* in noir-projects/labs/aztec-nr/aztec/src/authwit/account.nr. Derived from the poseidon hash of
* "az_dom_sep__entrypoint_payload" truncated to a u32; kept in TypeScript by hand because the generated constants
* package only carries protocol-circuit constants, and pinned by a drift test that re-derives it.
*/
export const ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR = 3045079954;

/**
* Computes the inner hash the account authorizes when invoked through its entrypoint. Binds the app payload
* together with the fee-payment selector and cancellation flag so that the account's approval covers the fee-payer
* and cancellation side effects rather than the call list alone.
*/
export async function computeEntrypointPayloadHash(
encodedCalls: EncodedAppEntrypointCalls,
feePaymentMethod: AccountFeePaymentMethodOptions,
cancellable: boolean,
): Promise<Fr> {
return poseidon2HashWithSeparator(
[await encodedCalls.hash(), new Fr(feePaymentMethod), new Fr(cancellable)],
ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR,
);
}

/**
* The mechanism via which an account contract will pay for a transaction in which it gets invoked.
*/
Expand Down Expand Up @@ -142,7 +167,7 @@ export class DefaultAccountEntrypoint implements EntrypointInterface {

const functionSelector = await FunctionSelector.fromNameAndParameters(abi.name, abi.parameters);

const payloadHash = await encodedCalls.hash();
const payloadHash = await computeEntrypointPayloadHash(encodedCalls, feePaymentMethodOptions, !!cancellable);
const messageHash = await computeOuterAuthWitHash(this.address, chainInfo.chainId, chainInfo.version, payloadHash);
const payloadAuthWitness = await this.auth.createAuthWit(messageHash);

Expand Down
Loading