From 45e4215e3002c47ae44328459914966e60809b06 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Fri, 13 Mar 2026 00:04:26 +0000 Subject: [PATCH 1/7] fix: stabilize e2e_bot test fees and fix L1 nonce races (backport #20962 #20992 #21148) BEGIN_COMMIT_OVERRIDE fix: set wallet minFeePadding in BotFactory constructor (#20992) fix: increase minFeePadding in e2e_bot bridge resume tests and harden GasFees.mul() (#20962) fix: use dedicated L1 account for bot bridge resume tests to avoid nonce race (#21148) fix: remove stale fee snapshot from BotFactory account deployment END_COMMIT_OVERRIDE Backports three fixes from next and adds a fourth: 1. BotFactory constructor now calls wallet.setMinFeePadding() so all setup transactions (token deploy, minting) use the configured padding instead of the wallet default (0.5x). (#20992) 2. GasFees.mul() uses bigint arithmetic for integer scalars to avoid precision loss, and Math.ceil for non-integer scalars. (#20962) 3. Bridge resume tests use a dedicated L1 private key (index 7) instead of the default mnemonic shared with the sequencer, avoiding nonce races on approve/deposit calls. (#21148) 4. Remove explicit maxFeesPerGas snapshot from setupAccountWithPrivateKey. Let the wallet re-estimate fees at send() time via completeFeeOptions(), making the fee estimate fresh and 10x padding sufficient. --- yarn-project/bot/src/factory.ts | 11 ++++++----- yarn-project/end-to-end/src/e2e_bot.test.ts | 21 +++++++++++++-------- yarn-project/stdlib/src/gas/gas_fees.ts | 8 +++++++- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index f37bffd1031c..db9e3dd4a4d0 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -27,7 +27,6 @@ import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -49,7 +48,11 @@ export class BotFactory { private readonly store: BotStore, private readonly aztecNode: AztecNode, private readonly aztecNodeAdmin?: AztecNodeAdmin, - ) {} + ) { + // Set fee padding on the wallet so that all transactions during setup + // (token deploy, minting, etc.) use the configured padding, not the default. + this.wallet.setMinFeePadding(config.minFeePadding); + } /** * Initializes a new bot by setting up the sender account, registering the recipient, @@ -218,13 +221,11 @@ export class BotFactory { const paymentMethod = new FeeJuicePaymentMethodWithClaim(accountManager.address, claim); const deployMethod = await accountManager.getDeployMethod(); - const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1 + this.config.minFeePadding); - const gasSettings = GasSettings.default({ maxFeesPerGas }); await this.withNoMinTxsPerBlock(async () => { const { txHash } = await deployMethod.send({ from: AztecAddress.ZERO, - fee: { gasSettings, paymentMethod }, + fee: { paymentMethod }, wait: NO_WAIT, }); this.log.info(`Sent tx for account deployment with hash ${txHash.toString()}`); diff --git a/yarn-project/end-to-end/src/e2e_bot.test.ts b/yarn-project/end-to-end/src/e2e_bot.test.ts index 00c5b06e16c3..45e5e37ad94a 100644 --- a/yarn-project/end-to-end/src/e2e_bot.test.ts +++ b/yarn-project/end-to-end/src/e2e_bot.test.ts @@ -52,6 +52,9 @@ describe('e2e_bot', () => { afterAll(() => teardown()); + let privateKeyIndex = 10; + const getPrivateKey = () => new SecretValue(bufferToHex(getPrivateKeyFromIndex(privateKeyIndex++)!)); + describe('transaction-bot', () => { let bot: Bot; beforeAll(async () => { @@ -131,10 +134,12 @@ describe('e2e_bot', () => { l1RpcUrls, feePaymentMethod: 'fee_juice', - // TODO: this should be taken from the `setup` call above - l1Mnemonic: new SecretValue('test test test test test test test test test test test junk'), + // Use a dedicated L1 account (index 7) for bridging. The default mnemonic account (index 0) + // is shared with the sequencer which sends L1 block proposals, causing nonce races on the + // approve/deposit calls in bridgeL1FeeJuice. + l1PrivateKey: new SecretValue(bufferToHex(getPrivateKeyFromIndex(7)!)), flushSetupTransactions: true, - // Increase fee headroom to handle fee volatility from rapid block building in tests + // Fee headroom to handle fee volatility from rapid block building in tests. minFeePadding: 9, }; @@ -171,10 +176,10 @@ describe('e2e_bot', () => { l1RpcUrls, feePaymentMethod: 'fee_juice', - // TODO: this should be taken from the `setup` call above - l1Mnemonic: new SecretValue('test test test test test test test test test test test junk'), + // Dedicated L1 account to avoid nonce races with the sequencer. + l1PrivateKey: new SecretValue(bufferToHex(getPrivateKeyFromIndex(7)!)), flushSetupTransactions: true, - // Increase fee headroom to handle fee volatility from rapid block building in tests + // Fee headroom to handle fee volatility from rapid block building in tests. minFeePadding: 9, }; @@ -235,7 +240,7 @@ describe('e2e_bot', () => { followChain: 'PROPOSED', botMode: 'transfer', senderPrivateKey: new SecretValue(Fr.random()), - l1PrivateKey: new SecretValue(bufferToHex(getPrivateKeyFromIndex(8)!)), + l1PrivateKey: getPrivateKey(), l1RpcUrls, flushSetupTransactions: true, }; @@ -258,7 +263,7 @@ describe('e2e_bot', () => { followChain: 'PROPOSED', botMode: 'crosschain', l1RpcUrls, - l1PrivateKey: new SecretValue(bufferToHex(getPrivateKeyFromIndex(9)!)), + l1PrivateKey: getPrivateKey(), flushSetupTransactions: true, l1ToL2SeedCount: 2, }; diff --git a/yarn-project/stdlib/src/gas/gas_fees.ts b/yarn-project/stdlib/src/gas/gas_fees.ts index ca2e700c27da..7387b2df0496 100644 --- a/yarn-project/stdlib/src/gas/gas_fees.ts +++ b/yarn-project/stdlib/src/gas/gas_fees.ts @@ -56,8 +56,14 @@ export class GasFees { return this.clone(); } else if (typeof scalar === 'bigint') { return new GasFees(this.feePerDaGas * scalar, this.feePerL2Gas * scalar); + } else if (Number.isInteger(scalar)) { + const s = BigInt(scalar); + return new GasFees(this.feePerDaGas * s, this.feePerL2Gas * s); } else { - return new GasFees(Number(this.feePerDaGas) * scalar, Number(this.feePerL2Gas) * scalar); + return new GasFees( + BigInt(Math.ceil(Number(this.feePerDaGas) * scalar)), + BigInt(Math.ceil(Number(this.feePerL2Gas) * scalar)), + ); } } From 3470f78f53be9a6f11278d8cd369c60ec14d60a6 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Thu, 12 Mar 2026 08:43:02 -0300 Subject: [PATCH 2/7] cherry-pick: feat!: auto-enqueue public init nullifier for contracts with public functions (#20775) Cherry-pick of cee97a1ba434f8469245152ca18d306f33d303fb with conflicts. --- .../docs/resources/migration_notes.md | 39 +++- .../aztec-nr/aztec/src/macros/aztec.nr | 26 ++- .../external_functions_stubs.nr | 2 +- .../aztec-nr/aztec/src/macros/dispatch.nr | 29 ++- .../src/macros/emit_public_init_nullifier.nr | 34 +++ .../macros/functions/initialization_utils.nr | 98 +++++++-- .../aztec/src/macros/functions/mod.nr | 54 ++++- .../external/private.nr | 7 +- .../external/public.nr | 5 +- .../aztec-nr/aztec/src/macros/mod.nr | 1 + .../aztec-nr/aztec/src/macros/utils.nr | 6 +- .../Nargo.toml | 7 + .../src/main.nr | 11 + .../reserved_public_dispatch/Nargo.toml | 7 + .../reserved_public_dispatch/src/main.nr | 11 + noir-projects/noir-contracts/Nargo.toml | 2 + .../test/init_test_contract/Nargo.toml | 8 + .../test/init_test_contract/src/main.nr | 77 +++++++ .../private_init_test_contract/Nargo.toml | 9 + .../private_init_test_contract/src/main.nr | 47 +++++ .../test/stateful_test_contract/src/main.nr | 20 +- .../test/updatable_contract/src/main.nr | 20 +- .../crates/types/src/constants.nr | 6 + .../crates/types/src/constants_tests.nr | 11 +- yarn-project/constants/src/constants.gen.ts | 1 + .../src/composed/ha/e2e_ha_full.test.ts | 83 +++++++- ...e2e_multi_validator_node_key_store.test.ts | 6 +- .../end-to-end/src/e2e_block_building.test.ts | 2 - .../e2e_deploy_contract/deploy_method.test.ts | 26 +++ .../src/e2e_deploy_contract/legacy.test.ts | 2 - .../private_initialization.test.ts | 197 ++++++++++++++---- .../end-to-end/src/e2e_multi_eoa.test.ts | 2 - .../e2e_multi_validator_node.test.ts | 4 - .../src/e2e_sequencer/reload_keystore.test.ts | 4 - .../src/e2e_sequencer_config.test.ts | 3 +- .../end-to-end/src/e2e_simple.test.ts | 2 - 36 files changed, 753 insertions(+), 116 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/macros/emit_public_init_nullifier.nr create mode 100644 noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/Nargo.toml create mode 100644 noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/src/main.nr create mode 100644 noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/Nargo.toml create mode 100644 noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/test/init_test_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/test/private_init_test_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/test/private_init_test_contract/src/main.nr diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 6d599bccc6b1..ab9c6227be70 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,44 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +<<<<<<< HEAD +======= +### Two separate init nullifiers for private and public + +Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain: + +- Private external functions check the private init nullifier. +- Public external functions check the public init nullifier. + +**How initializers work:** + +- **Private initializers** emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier. +- **Public initializers** emit both nullifiers directly. +- Contracts with no public functions only emit the private init nullifier. + +**`only_self` functions no longer have init checks.** They behave as if marked `noinitcheck`. + +**External functions called during private initialization must be `#[only_self]`.** Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via `enqueue_self` or `call_self`) during initialization will fail the init check unless they skip it. + +**Breaking change for deployment:** If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass `skipClassPublication: true`, because the auto-enqueued public call requires the class to be available. + +```diff + const deployed = await MyContract.deploy(wallet, ...args).send({ +- skipClassPublication: true, + }).deployed(); +``` + +### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained + +This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. + +### [Aztec.nr] Changes to standard note hash computation + +Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation. + +This change requires no input from your side unless you were testing or relying on hardcoded note hashes. + +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) ### [Aztec.js] `getPublicEvents` now returns an object instead of an array `getPublicEvents` now returns a `GetPublicEventsResult` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option. @@ -23,7 +61,6 @@ The `maxLogsHit` flag indicates whether the log limit was reached, meaning more ### [Aztec.nr] Removed `get_random_bytes` The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself. - ### [Aztec.js] `simulate()`, `send()`, and deploy return types changed to always return objects All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects `.simulate()`, `.send()`, deploy `.send()`, and `Wallet.sendTx()`. diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index b7b27b9c3d16..7f58bce688de 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -1,3 +1,4 @@ +<<<<<<< HEAD use crate::macros::{ calls_generation::{ external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs}, @@ -10,6 +11,23 @@ use crate::macros::{ utils::{ get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, module_has_storage, +======= +use crate::{ + macros::{ + calls_generation::{ + external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs}, + internal_functions::generate_call_internal_struct, + }, + dispatch::generate_public_dispatch, + emit_public_init_nullifier::generate_emit_public_init_nullifier, + internals_functions_generation::{create_fn_abi_exports, process_functions}, + notes::NOTES, + storage::STORAGE_LAYOUT_NAME, + utils::{ + get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, + module_has_storage, + }, +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) }, }; @@ -53,7 +71,8 @@ pub comptime fn aztec(m: Module) -> Quoted { } else { quote {} }; - let public_dispatch = generate_public_dispatch(m); + let (has_public_init_nullifier_fn, emit_public_init_nullifier_fn_body) = generate_emit_public_init_nullifier(m); + let public_dispatch = generate_public_dispatch(m, has_public_init_nullifier_fn); quote { $interface @@ -65,6 +84,11 @@ pub comptime fn aztec(m: Module) -> Quoted { $public_dispatch $sync_state_fn_and_abi_export $process_message_fn_and_abi_export +<<<<<<< HEAD +======= + $emit_public_init_nullifier_fn_body + $offchain_receive_fn_and_abi_export +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) } } diff --git a/noir-projects/aztec-nr/aztec/src/macros/calls_generation/external_functions_stubs.nr b/noir-projects/aztec-nr/aztec/src/macros/calls_generation/external_functions_stubs.nr index 068f6c8cbcbe..513ea3593ba8 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/calls_generation/external_functions_stubs.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/calls_generation/external_functions_stubs.nr @@ -38,7 +38,7 @@ comptime fn create_stub_base(f: FunctionDefinition) -> (Quoted, Quoted, Quoted, let fn_name_str = f"\"{fn_name}\"".quoted_contents(); let fn_name_len: u32 = unquote!(quote { $fn_name_str.as_bytes().len()}); - let fn_selector: Field = compute_fn_selector(f); + let fn_selector: Field = compute_fn_selector(f.name(), f.parameters()); ( fn_name, fn_parameters_list, serialized_args_array_construction, serialized_args_array_name, diff --git a/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr b/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr index 52175d808070..fe9dcd16d80d 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr @@ -1,23 +1,28 @@ use crate::macros::internals_functions_generation::external_functions_registry::get_public_functions; use crate::protocol::meta::utils::get_params_len_quote; use crate::utils::cmap::CHashMap; +use super::functions::initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME; use super::utils::compute_fn_selector; use std::panic; -/// Returns an `fn public_dispatch(...)` function for the given module that's assumed to be an Aztec contract. -pub comptime fn generate_public_dispatch(m: Module) -> Quoted { +/// Generates a `public_dispatch` function for an Aztec contract module `m`. +/// +/// The generated function dispatches public calls based on selector to the appropriate contract function. If +/// `generate_emit_public_init_nullifier` is true, it also handles dispatch to the macro-generated +/// `__emit_public_init_nullifier` function. +pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nullifier: bool) -> Quoted { let functions = get_public_functions(m); let unit = get_type::<()>(); let seen_selectors = &mut CHashMap::::new(); - let ifs = functions.map(|function: FunctionDefinition| { + let mut ifs = functions.map(|function: FunctionDefinition| { let parameters = function.parameters(); let return_type = function.return_type(); - let selector: Field = compute_fn_selector(function); let fn_name = function.name(); + let selector: Field = compute_fn_selector(fn_name, parameters); // Since function selectors are computed as the first 4 bytes of the hash of the function signature, it's // possible to have collisions. With the following check, we ensure it doesn't happen within the same contract. @@ -96,6 +101,22 @@ pub comptime fn generate_public_dispatch(m: Module) -> Quoted { if_ }); + // If we injected the auto-generated public function to emit the public initialization nullifier, then + // we'll also need to handle its dispatch. + if generate_emit_public_init_nullifier { + let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME; + let init_nullifier_selector: Field = compute_fn_selector(name, @[]); + + ifs = ifs.push_back( + quote { + if selector == $init_nullifier_selector { + $name(); + aztec::oracle::avm::avm_return([]); + } + }, + ); + } + if ifs.len() == 0 { // No dispatch function if there are no public functions quote {} diff --git a/noir-projects/aztec-nr/aztec/src/macros/emit_public_init_nullifier.nr b/noir-projects/aztec-nr/aztec/src/macros/emit_public_init_nullifier.nr new file mode 100644 index 000000000000..1e39bba588b2 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/macros/emit_public_init_nullifier.nr @@ -0,0 +1,34 @@ +use crate::macros::{ + functions::initialization_utils::{EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME, has_public_init_checked_functions}, + internals_functions_generation::external_functions_registry::get_private_functions, + utils::is_fn_initializer, +}; + +/// Returns `(has_public_init_nullifier_fn, fn_body)` for the auto-generated public init nullifier function. +/// +/// Contracts with a private initializer and public functions that check initialization need an auto-generated public +/// function to emit the public init nullifier. If these conditions are met, returns `(true, fn_body_quoted)`; +/// otherwise returns `(false, quote {})`. +pub(crate) comptime fn generate_emit_public_init_nullifier(m: Module) -> (bool, Quoted) { + let has_private_initializer = get_private_functions(m).any(|f: FunctionDefinition| is_fn_initializer(f)); + let has_public_fns_with_init_check = has_public_init_checked_functions(m); + + if has_private_initializer & has_public_fns_with_init_check { + let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME; + let assertion_message = f"Function {name} can only be called by the same contract".as_quoted_str(); + let body = quote { + #[aztec::macros::internals_functions_generation::abi_attributes::abi_public] + unconstrained fn $name() { + let context = aztec::context::PublicContext::new(|| { 0 }); + assert( + context.maybe_msg_sender().unwrap() == context.this_address(), + $assertion_message, + ); + aztec::macros::functions::initialization_utils::mark_as_initialized_public(context); + } + }; + (true, body) + } else { + (false, quote {}) + } +} diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr index 351684905292..7c816adfda32 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/initialization_utils.nr @@ -1,47 +1,117 @@ use crate::protocol::{ - abis::function_selector::FunctionSelector, address::AztecAddress, constants::DOM_SEP__INITIALIZER, - hash::poseidon2_hash_with_separator, traits::ToField, + abis::function_selector::FunctionSelector, + address::AztecAddress, + constants::{DOM_SEP__INITIALIZER, DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER}, + hash::poseidon2_hash_with_separator, + traits::ToField, }; +use std::meta::{ctstring::AsCtString, unquote}; use crate::{ context::{PrivateContext, PublicContext}, + macros::{ + internals_functions_generation::external_functions_registry::get_public_functions, utils::fn_has_noinitcheck, + }, nullifier::utils::compute_nullifier_existence_request, oracle::get_contract_instance::{ get_contract_instance, get_contract_instance_deployer_avm, get_contract_instance_initialization_hash_avm, }, }; -// Used by `create_mark_as_initialized` (you won't find it through searching) +/// The name of the auto-generated function that emits the public init nullifier. +/// +/// This function is injected into the public dispatch table for contracts with initializers. +pub(crate) comptime global EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME: Quoted = quote { __emit_public_init_nullifier }; + +/// Returns `true` if the module has any public functions that require initialization checks (i.e. that don't have +/// `#[noinitcheck]`). If all public functions skip init checks, there's no point emitting the public init nullifier +/// since nothing will check it. +pub(crate) comptime fn has_public_init_checked_functions(m: Module) -> bool { + get_public_functions(m).any(|f: FunctionDefinition| !fn_has_noinitcheck(f)) +} + +/// Selector for `EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME`, derived at comptime so it stays in sync. +global EMIT_PUBLIC_INIT_NULLIFIER_SELECTOR: FunctionSelector = comptime { + let name = EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME; + let sig = f"{name}()".as_ctstring(); + unquote!(quote { FunctionSelector::from_signature($sig) }) +}; + +/// Emits (only) the public initialization nullifier. +/// +/// This function is called by the aztec-nr auto-generated external public contract function (enqueued by private +/// [`initializer`](crate::macros::functions::initializer) functions), and also by +/// [`mark_as_initialized_from_public_initializer`] for public initializers. +/// +/// # Warning +/// +/// This should not be called manually. Incorrect use can leave the contract in a broken initialization state (e.g. +/// emitting the public nullifier without the private one). The macro-generated code handles this automatically. pub fn mark_as_initialized_public(context: PublicContext) { - let init_nullifier = compute_unsiloed_contract_initialization_nullifier((context).this_address()); + let init_nullifier = compute_public_init_nullifier(context.this_address()); context.push_nullifier(init_nullifier); } -// Used by `create_mark_as_initialized` (you won't find it through searching) -pub fn mark_as_initialized_private(context: &mut PrivateContext) { - let init_nullifier = compute_unsiloed_contract_initialization_nullifier((*context).this_address()); +fn mark_as_initialized_private(context: &mut PrivateContext) { + let init_nullifier = compute_private_init_nullifier((*context).this_address()); context.push_nullifier(init_nullifier); } -// Used by `create_init_check` (you won't find it through searching) +/// Emits the private initialization nullifier and, if relevant, enqueues the emission of the public one. +/// +/// If the contract has public functions that perform initialization checks (i.e. that don't have `#[noinitcheck]`), +/// this also enqueues a call to the auto-generated `__emit_public_init_nullifier` function so the public nullifier is +/// emitted in public. Called by private [`initializer`](crate::macros::functions::initializer) macros. +pub fn mark_as_initialized_from_private_initializer(context: &mut PrivateContext, emit_public_init_nullifier: bool) { + mark_as_initialized_private(context); + if emit_public_init_nullifier { + context.call_public_function((*context).this_address(), EMIT_PUBLIC_INIT_NULLIFIER_SELECTOR, [], false); + } +} + +/// Emits both initialization nullifiers (private and public). +/// +/// Called by public [`initializer`](crate::macros::functions::initializer) macros, since public initializers must set +/// both so that both private and public functions see the contract as initialized. +pub fn mark_as_initialized_from_public_initializer(context: PublicContext) { + let private_nullifier = compute_private_init_nullifier(context.this_address()); + context.push_nullifier(private_nullifier); + mark_as_initialized_public(context); +} + +/// Asserts that the contract has been initialized, from public's perspective. +/// +/// Checks that the public initialization nullifier exists. pub fn assert_is_initialized_public(context: PublicContext) { - let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context.this_address()); - // Safety: TODO(F-239) - this is currently unsafe, we cannot rely on the nullifier existing to determine that any - // public component of contract initialization has been complete. + let init_nullifier = compute_public_init_nullifier(context.this_address()); + // Safety: the public init nullifier is only ever emitted by public functions, and so the timing concerns from + // nullifier_exists_unsafe do not apply. Additionally, it is emitted after all initializer functions have run, + // so initialization is guaranteed to be complete by the time it exists. assert(context.nullifier_exists_unsafe(init_nullifier, context.this_address()), "Not initialized"); } -// Used by `create_init_check` (you won't find it through searching) +/// Asserts that the contract has been initialized, from private's perspective. +/// +/// Checks that the private initialization nullifier exists. pub fn assert_is_initialized_private(context: &mut PrivateContext) { - let init_nullifier = compute_unsiloed_contract_initialization_nullifier(context.this_address()); + let init_nullifier = compute_private_init_nullifier(context.this_address()); let nullifier_existence_request = compute_nullifier_existence_request(init_nullifier, context.this_address()); context.assert_nullifier_exists(nullifier_existence_request); } -fn compute_unsiloed_contract_initialization_nullifier(address: AztecAddress) -> Field { +// TODO(F-194): This leaks whether a contract has been initialized, since anyone who knows the address can compute this +// nullifier and check for its existence. It is also not domain separated. +fn compute_private_init_nullifier(address: AztecAddress) -> Field { address.to_field() } +fn compute_public_init_nullifier(address: AztecAddress) -> Field { + poseidon2_hash_with_separator( + [address.to_field()], + DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, + ) +} + // Used by `create_assert_correct_initializer_args` (you won't find it through searching) pub fn assert_initialization_matches_address_preimage_public(context: PublicContext) { let address = context.this_address(); diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index 75ab13d794ee..b9db8529f058 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -9,6 +9,7 @@ use crate::macros::{ }; use super::utils::{fn_has_allow_phase_change, fn_has_noinitcheck, is_fn_initializer, is_fn_only_self, is_fn_view}; use auth_registry::AUTHORIZE_ONCE_REGISTRY; +use initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME; // Functions can have multiple attributes applied to them, e.g. a single function can have #[external("public")], // #[view] and #[only_self]. However. the order in which this will be evaluated is unknown, which makes combining them @@ -29,7 +30,7 @@ use auth_registry::AUTHORIZE_ONCE_REGISTRY; /// /// Initializer functions are similar to constructors: /// - can only be called once -/// - [`external`] non-initializer functions cannot be called until one of the initializers has ben called +/// - [`external`] non-initializer functions cannot be called until one of the initializers has been called /// /// The only exception are [`noinitcheck`] functions, which can be called even if none of the initializers has been /// called. @@ -63,17 +64,26 @@ use auth_registry::AUTHORIZE_ONCE_REGISTRY; /// initialization by marking them with the `#[noinitcheck]` attribute - though any contract state initialization will /// of course not have taken place. /// -/// ## Cost +/// ## How It Works +/// +/// Initializers emit nullifiers to mark the contract as initialized. Two separate nullifiers are used (a private init +/// nullifier and a public init nullifier) because private nullifiers are committed before public execution +/// begins: if a single nullifier were used, public functions enqueued by the initializer would see it as +/// existing *before* the public initialization code had a chance to run. /// -/// The initialization process emits a nullifier which marks the contract as initialized. All other [`external`] -/// functions are automatically made to check that this nullifier exists, ensuring initialization. +/// - **Private initializers** emit the private init nullifier. For contracts that also have external public functions, +/// they auto-enqueue a call to an auto-generated public function that emits the public init nullifier during public +/// execution. This function name is reserved and cannot be used by contract developers. +/// - **Public initializers** emit both nullifiers directly. +/// - **Private external functions** check the private init nullifier. +/// - **Public external functions** check the public init nullifier. /// -/// For private non-initializer functions, the cost of this check is equivalent to that of a call to +/// For private non-initializer functions, the cost of this check is equivalent to a call to /// [`PrivateContext::assert_nullifier_exists`](crate::context::PrivateContext::assert_nullifier_exists). For public /// ones, it is equivalent to a call to /// [`PublicContext::nullifier_exists_unsafe`](crate::context::PublicContext::nullifier_exists_unsafe). /// -/// The [`noinitcheck`] attribute can be used to skip the initialization nullifer checks. +/// The [`noinitcheck`] attribute can be used to skip these checks. [`only_self`] functions also implicitly skip them. pub comptime fn initializer(f: FunctionDefinition) { // Marker attribute - see the comment at the top of this file @@ -185,6 +195,24 @@ pub comptime fn allow_phase_change(f: FunctionDefinition) { /// /// A classic example is a private mint function in a token, which would require an enqueued public call in which the /// total supply is incremented. +/// +/// ## Initialization Checks +/// +/// `only_self` functions implicitly skip initialization checks (as if they had [`noinitcheck`]). We want +/// `only_self` functions to be callable during initialization, so we can't have them check the init nullifier +/// since it would fail. +/// +/// This is safe because `only_self` functions can be called only by the contract the function is in, meaning +/// execution must start with another external function in the same contract. Eventually the call stack reaches the +/// `only_self` function, but let's focus on that external entry point: +/// - If it already performed an init check, then we are safe. +/// - If it skipped the init check (via [`noinitcheck`]), then the contract developer is explicitly choosing to not +/// check for initialization, and so will our `only_self` function. That's a design choice by the developer. If +/// we didn't skip the init check on `only_self`, the developer would just add `noinitcheck` to it anyway. +/// - If it was the initializer, note that init nullifiers are emitted at the end of initialization: the private +/// init nullifier after all private execution, and the public one after all public execution. So in terms of +/// init checking, everything behaves as if the contract hasn't been initialized yet, and the same two points +/// above still apply. pub comptime fn only_self(f: FunctionDefinition) { // Marker attribute - see the comment at the top of this file @@ -197,6 +225,8 @@ pub comptime fn only_self(f: FunctionDefinition) { f"The #[only_self] attribute can only be applied to #[external(\"private\")] or #[external(\"public\")] functions - {name} is neither", ); } + + f.add_attribute("noinitcheck"); } /// View functions cannot modify state in any way, including performing contract calls that would in turn modify state. @@ -379,6 +409,18 @@ comptime fn assert_valid_public(f: FunctionDefinition) { f"The #[allow_phase_change] attribute cannot be applied to #[external(\"public\")] functions - {name}", ); } + + if f.name() == EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME { + panic( + f"Function name '{EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME}' is reserved for internal use", + ); + } + + if f.name() == quote { public_dispatch } { + panic( + f"Function name 'public_dispatch' is reserved for internal use", + ); + } } comptime fn assert_valid_utility(f: FunctionDefinition) { diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr index 88c0f8590d4d..6f9d8d8592c1 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr @@ -1,4 +1,5 @@ use crate::macros::{ + functions::initialization_utils::has_public_init_checked_functions, internals_functions_generation::external::helpers::{create_authorize_once_check, get_abi_relevant_attributes}, utils::{ fn_has_allow_phase_change, fn_has_authorize_once, fn_has_noinitcheck, is_fn_initializer, is_fn_only_self, @@ -77,9 +78,10 @@ pub(crate) comptime fn generate_private_external(f: FunctionDefinition) -> Quote }; let (assert_initializer, mark_as_initialized) = if is_fn_initializer(f) { + let has_public_fns_with_init_check = has_public_init_checked_functions(f.module()); ( quote { aztec::macros::functions::initialization_utils::assert_initialization_matches_address_preimage_private(*self.context); }, - quote { aztec::macros::functions::initialization_utils::mark_as_initialized_private(self.context); }, + quote { aztec::macros::functions::initialization_utils::mark_as_initialized_from_private_initializer(self.context, $has_public_fns_with_init_check); }, ) } else { (quote {}, quote {}) @@ -177,6 +179,9 @@ pub(crate) comptime fn generate_private_external(f: FunctionDefinition) -> Quote let body_quote = body.map(|expr| expr.quoted()).join(quote { }); + // `mark_as_initialized` is placed after the user's function body. If it ran at the beginning, the contract + // would appear initialized while the initializer is still running, allowing contracts called by the initializer + // to re-enter into a half-initialized contract. let to_append = quote { $return_value $mark_as_initialized diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr index d481fd0a9666..0eaf0adea0ea 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr @@ -79,7 +79,7 @@ pub(crate) comptime fn generate_public_external(f: FunctionDefinition) -> Quoted let (assert_initializer, mark_as_initialized) = if is_fn_initializer(f) { ( quote { aztec::macros::functions::initialization_utils::assert_initialization_matches_address_preimage_public(self.context); }, - quote { aztec::macros::functions::initialization_utils::mark_as_initialized_public(self.context); }, + quote { aztec::macros::functions::initialization_utils::mark_as_initialized_from_public_initializer(self.context); }, ) } else { (quote {}, quote {}) @@ -108,6 +108,9 @@ pub(crate) comptime fn generate_public_external(f: FunctionDefinition) -> Quoted $authorize_once_check }; + // `mark_as_initialized` is placed after the user's function body. If it ran at the beginning, the contract + // would appear initialized while the initializer is still running, allowing contracts called by the initializer + // to re-enter into a half-initialized contract. let to_append = quote { $mark_as_initialized }; diff --git a/noir-projects/aztec-nr/aztec/src/macros/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/mod.nr index 50f17bc3250a..41f1e0a6904d 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/mod.nr @@ -3,6 +3,7 @@ pub mod aztec; pub mod dispatch; pub(crate) mod calls_generation; +pub(crate) mod emit_public_init_nullifier; pub mod internals_functions_generation; pub mod functions; pub mod utils; diff --git a/noir-projects/aztec-nr/aztec/src/macros/utils.nr b/noir-projects/aztec-nr/aztec/src/macros/utils.nr index 151079bb208b..40cdd20251c9 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/utils.nr @@ -85,15 +85,15 @@ comptime fn signature_of_type(typ: Type) -> CtString { } } -pub(crate) comptime fn compute_fn_selector(f: FunctionDefinition) -> Field { +/// Computes a function selector from a name and parameter list. +pub(crate) comptime fn compute_fn_selector(fn_name: Quoted, parameters: [(Quoted, Type)]) -> Field { // The function selector is computed from the function signature, which is made up of the function name and types // of parameters, but not including the return type. For example, given: // // fn foo(a: Field, b: AztecAddress) -> Field // // The signature will be "foo(Field,AztecAddress)". - let fn_name = f.name(); - let args_signatures = f.parameters().map(|(_, typ)| signature_of_type(typ)).join(",".as_ctstring()); + let args_signatures = parameters.map(|(_, typ)| signature_of_type(typ)).join(",".as_ctstring()); let signature_quote = f"{fn_name}({args_signatures})".as_ctstring(); let computation_quote = quote { diff --git a/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/Nargo.toml b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/Nargo.toml new file mode 100644 index 000000000000..1cfbb552131b --- /dev/null +++ b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "reserved_emit_public_init_nullifier" +type = "contract" +authors = [""] + +[dependencies] +aztec = { path = "../../../aztec" } diff --git a/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/src/main.nr b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/src/main.nr new file mode 100644 index 000000000000..8fefacdd7369 --- /dev/null +++ b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_emit_public_init_nullifier/src/main.nr @@ -0,0 +1,11 @@ +/// Public functions cannot use the reserved name __emit_public_init_nullifier. +/// +use aztec::macros::aztec; + +#[aztec] +contract ReservedEmitPublicInitNullifier { + use aztec::macros::functions::external; + + #[external("public")] + fn __emit_public_init_nullifier() {} +} diff --git a/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/Nargo.toml b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/Nargo.toml new file mode 100644 index 000000000000..e6c4a85c297f --- /dev/null +++ b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "reserved_public_dispatch" +type = "contract" +authors = [""] + +[dependencies] +aztec = { path = "../../../aztec" } diff --git a/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/src/main.nr b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/src/main.nr new file mode 100644 index 000000000000..f3493f9ccfa3 --- /dev/null +++ b/noir-projects/aztec-nr/macro_compilation_failure_tests/failure_contracts/reserved_public_dispatch/src/main.nr @@ -0,0 +1,11 @@ +/// Public functions cannot use the reserved name public_dispatch. +/// +use aztec::macros::aztec; + +#[aztec] +contract ReservedPublicDispatch { + use aztec::macros::functions::external; + + #[external("public")] + fn public_dispatch() {} +} diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 7b7c76bf8bad..c0afb35f7b34 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -46,6 +46,7 @@ members = [ "contracts/test/counter_contract", "contracts/test/event_only_contract", "contracts/test/import_test_contract", + "contracts/test/init_test_contract", "contracts/test/invalid_account_contract", "contracts/test/no_constructor_contract", "contracts/test/note_getter_contract", @@ -55,6 +56,7 @@ members = [ "contracts/test/oracle_version_check_contract", "contracts/test/parent_contract", "contracts/test/pending_note_hashes_contract", + "contracts/test/private_init_test_contract", "contracts/test/public_immutable_contract", "contracts/test/returning_tuple_contract", "contracts/test/scope_test_contract", diff --git a/noir-projects/noir-contracts/contracts/test/init_test_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/init_test_contract/Nargo.toml new file mode 100644 index 000000000000..2d378fe1845a --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/init_test_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "init_test_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr new file mode 100644 index 000000000000..2120f72e0fa5 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/init_test_contract/src/main.nr @@ -0,0 +1,77 @@ +// A contract with both private and public functions used to test initialization behavior, including init checks, +// self-calls during initialization, and public/private ordering. +use aztec::macros::aztec; + +#[aztec] +pub contract InitTest { + use aztec::macros::{ + functions::{external, initializer, noinitcheck, only_self, view}, + storage::storage, + }; + use aztec::protocol::address::AztecAddress; + use aztec::state_vars::{Map, PublicMutable}; + + #[storage] + struct Storage { + public_values: Map, Context>, + } + + #[external("private")] + #[initializer] + fn constructor(owner: AztecAddress, value: Field) {} + + #[external("private")] + #[initializer] + fn initializer_self_calling_private_not_init_checked(owner: AztecAddress, value: Field) { + self.call_self.priv_no_init_check(owner, value); + } + + #[external("private")] + #[initializer] + fn initializer_self_calling_private_init_checked(owner: AztecAddress, value: Field) { + self.call_self.priv_init_check(owner, value); + } + + #[external("private")] + #[initializer] + fn initializer_enqueuing_public_init_checked(owner: AztecAddress, value: Field) { + self.enqueue_self.pub_init_check(owner, value); + } + + #[external("private")] + #[initializer] + fn initializer_calling_only_self(owner: AztecAddress, value: Field) { + self.call_self.priv_only_self(owner, value); + } + + // Tests that a public initializer cannot self-call a public function that has init checks, since the contract is + // not yet initialized at that point. + #[external("public")] + #[initializer] + fn public_initializer_self_calling_init_checked(owner: AztecAddress, value: Field) { + self.call_self.pub_init_check(owner, value); + } + + #[external("private")] + #[only_self] + fn priv_only_self(owner: AztecAddress, value: Field) {} + + #[external("private")] + fn priv_init_check(owner: AztecAddress, value: Field) {} + + #[external("private")] + #[noinitcheck] + fn priv_no_init_check(owner: AztecAddress, value: Field) {} + + #[external("public")] + fn pub_init_check(owner: AztecAddress, value: Field) { + self.storage.public_values.at(owner).write(value); + } + + #[external("public")] + #[noinitcheck] + #[view] + fn pub_no_init_check(owner: AztecAddress) -> pub Field { + self.storage.public_values.at(owner).read() + } +} diff --git a/noir-projects/noir-contracts/contracts/test/private_init_test_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/private_init_test_contract/Nargo.toml new file mode 100644 index 000000000000..3414de8c017f --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/private_init_test_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "private_init_test_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } +field_note = { path = "../../../../aztec-nr/field-note" } diff --git a/noir-projects/noir-contracts/contracts/test/private_init_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/private_init_test_contract/src/main.nr new file mode 100644 index 000000000000..f4eadfc6292f --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/private_init_test_contract/src/main.nr @@ -0,0 +1,47 @@ +// A private-only contract (no public functions) used to test initialization behavior. Since it has no public +// functions, it does not need to be published on-chain to be tested. +use aztec::macros::aztec; + +#[aztec] +pub contract PrivateInitTest { + use aztec::{ + macros::{functions::{external, initializer, noinitcheck}, storage::storage}, + messages::message_delivery::MessageDelivery, + protocol::address::AztecAddress, + state_vars::{Owned, PrivateMutable}, + }; + use field_note::FieldNote; + + #[storage] + struct Storage { + value: Owned, Context>, + } + + #[initializer] + #[external("private")] + fn initialize(initial_value: Field) { + let owner = self.msg_sender(); + self.storage.value.at(owner).initialize(FieldNote { value: initial_value }).deliver( + MessageDelivery.ONCHAIN_CONSTRAINED, + ); + } + + #[external("private")] + fn private_init_check_write_value(new_value: Field) { + let owner = self.msg_sender(); + self.storage.value.at(owner).replace(|_old| FieldNote { value: new_value }).deliver( + MessageDelivery.ONCHAIN_CONSTRAINED, + ); + } + + #[external("private")] + #[noinitcheck] + fn private_no_init_check_emit_nullifier(nullifier: Field) { + self.context.push_nullifier(nullifier); + } + + #[external("utility")] + unconstrained fn utility_read_value(owner: AztecAddress) -> Field { + self.storage.value.at(owner).view_note().value + } +} diff --git a/noir-projects/noir-contracts/contracts/test/stateful_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/stateful_test_contract/src/main.nr index 4baa4a76d448..6f0d7ba3a87e 100644 --- a/noir-projects/noir-contracts/contracts/test/stateful_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/stateful_test_contract/src/main.nr @@ -3,13 +3,7 @@ use aztec::macros::aztec; #[aztec] pub contract StatefulTest { - use aztec::macros::{ - functions::{ - external, initialization_utils::assert_is_initialized_private, initializer, noinitcheck, - view, - }, - storage::storage, - }; + use aztec::macros::{functions::{external, initializer, noinitcheck, view}, storage::storage}; use aztec::{ messages::message_delivery::MessageDelivery, note::{note_getter_options::NoteGetterOptions, note_viewer_options::NoteViewerOptions}, @@ -60,18 +54,6 @@ pub contract StatefulTest { } } - #[external("private")] - fn destroy_and_create(recipient: AztecAddress) { - assert_is_initialized_private(self.context); - let sender = self.msg_sender(); - - let _ = self.storage.notes.at(sender).pop_notes(NoteGetterOptions::new().set_limit(2)); - - self.storage.notes.at(recipient).insert(FieldNote { value: 92 }).deliver( - MessageDelivery.ONCHAIN_CONSTRAINED, - ); - } - #[external("private")] #[noinitcheck] fn destroy_and_create_no_init_check(recipient: AztecAddress) { diff --git a/noir-projects/noir-contracts/contracts/test/updatable_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/updatable_contract/src/main.nr index 317e381aa137..08ed569b0d7a 100644 --- a/noir-projects/noir-contracts/contracts/test/updatable_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/updatable_contract/src/main.nr @@ -1,10 +1,11 @@ use aztec::macros::aztec; -/// A contract that can be updated to a new contract class. Used to test contract updates in e2e_contract_updates.test.ts. +/// A contract that can be updated to a new contract class. Used to test contract updates in +/// e2e_contract_updates.test.ts. #[aztec] contract Updatable { use aztec::{ - macros::{functions::{external, initializer}, storage::storage}, + macros::{functions::{external, initializer, internal, only_self}, storage::storage}, messages::message_delivery::MessageDelivery, protocol::{ address::AztecAddress, constants::CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, @@ -29,12 +30,18 @@ contract Updatable { self.storage.private_value.at(owner).initialize(note).deliver( MessageDelivery.ONCHAIN_CONSTRAINED, ); - self.enqueue_self.set_public_value(initial_value); + self.enqueue_self._initialize_public_value(initial_value); } #[external("public")] fn set_public_value(new_value: Field) { - self.storage.public_value.write(new_value); + self.internal.write_public_value(new_value); + } + + #[external("public")] + #[only_self] + fn _initialize_public_value(new_value: Field) { + self.internal.write_public_value(new_value); } #[external("private")] @@ -55,6 +62,11 @@ contract Updatable { .get_update_delay()) } + #[internal("public")] + fn write_public_value(new_value: Field) { + self.storage.public_value.write(new_value); + } + #[external("utility")] unconstrained fn get_private_value(owner: AztecAddress) -> Field { self.storage.private_value.at(owner).view_note().value diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 6100302d21ee..b5a95fc2b566 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -713,6 +713,12 @@ pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; // TODO: consider moving these to aztec-nr pub global DOM_SEP__INITIALIZATION_NULLIFIER: u32 = 1653084894; +<<<<<<< HEAD +======= +pub global DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER: u32 = 3342006647; + +/// Domain separator for L1 to L2 message secret hashes. +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) pub global DOM_SEP__SECRET_HASH: u32 = 4199652938; pub global DOM_SEP__TX_NULLIFIER: u32 = 1025801951; pub global DOM_SEP__SIGNATURE_PAYLOAD: u32 = 463525807; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index 6f7c9dee1cc1..5025b22a56cf 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -14,7 +14,8 @@ use crate::{ DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, - DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_KEYS_HASH, + DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, + DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, @@ -129,7 +130,11 @@ impl HashedValueTester::new(); +======= + let mut tester = HashedValueTester::<52, 45>::new(); +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) // ----------------- // Domain separators @@ -183,6 +188,10 @@ fn hashed_values_match_derived() { DOM_SEP__INITIALIZATION_NULLIFIER, "initialization_nullifier", ); + tester.assert_dom_sep_matches_derived( + DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, + "public_initialization_nullifier", + ); tester.assert_dom_sep_matches_derived(DOM_SEP__SECRET_HASH, "secret_hash"); tester.assert_dom_sep_matches_derived(DOM_SEP__TX_NULLIFIER, "tx_nullifier"); tester.assert_dom_sep_matches_derived(DOM_SEP__SIGNATURE_PAYLOAD, "signature_payload"); diff --git a/yarn-project/constants/src/constants.gen.ts b/yarn-project/constants/src/constants.gen.ts index 6675dfa2cca1..c192fb14654e 100644 --- a/yarn-project/constants/src/constants.gen.ts +++ b/yarn-project/constants/src/constants.gen.ts @@ -541,6 +541,7 @@ export enum DomainSeparator { CIPHERTEXT_FIELD_MASK = 1870492847, PARTIAL_NOTE_VALIDITY_COMMITMENT = 623934423, INITIALIZATION_NULLIFIER = 1653084894, + PUBLIC_INITIALIZATION_NULLIFIER = 3342006647, SECRET_HASH = 4199652938, TX_NULLIFIER = 1025801951, SIGNATURE_PAYLOAD = 463525807, diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts index 279b42a40de6..391531da1583 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts @@ -282,8 +282,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - skipClassPublication: true, - skipInstancePublication: true, wait: { returnReceipt: true }, }); @@ -493,6 +491,85 @@ describe('HA Full Setup', () => { logger.info('Governance voting with HA coordination and L1 verification complete'); }); +<<<<<<< HEAD +======= + it('should reload keystore via admin API and keep building blocks after swapping attesters', async () => { + logger.info('Testing reloadKeystore: swap all attesters across HA nodes'); + + const groupA = attesterAddresses.slice(0, 2); + const groupB = attesterAddresses.slice(2, 4); + + const writeKeystoreForNode = async (nodeIdx: number, attesters: string[]) => { + const ks = { + schemaVersion: 1, + validators: [ + { + attester: attesters, + feeRecipient: AztecAddress.ZERO.toString(), + coinbase: EthAddress.fromString(attesters[0]).toChecksumString(), + remoteSigner: web3SignerUrl, + publisher: [publisherAddresses[nodeIdx]], + }, + ], + }; + await writeFile(join(haKeystoreDirs[nodeIdx], 'keystore.json'), JSON.stringify(ks, null, 2)); + }; + + const verifyNodeAttesters = (nodeIdx: number, expectedAttesters: string[], label: string) => { + const vc: ValidatorClient = (haNodeServices[nodeIdx] as any).validatorClient; + const addrs = vc.getValidatorAddresses(); + expect(addrs).toHaveLength(expectedAttesters.length); + for (const expected of expectedAttesters) { + expect(addrs.some(a => a.equals(EthAddress.fromString(expected)))).toBe(true); + } + logger.info(`Node ${nodeIdx}: ${addrs.length} attesters (${label})`); + }; + + const quorum = Math.floor((COMMITTEE_SIZE * 2) / 3) + 1; + + try { + // Phase 1: Nodes 0,1,2 get attesters [A0,A1], nodes 3,4 get [A2,A3] + logger.info('Phase 1: Initial attester split'); + for (let i = 0; i < NODE_COUNT; i++) { + await writeKeystoreForNode(i, i < 3 ? groupA : groupB); + await haNodeServices[i].reloadKeystore(); + } + for (let i = 0; i < NODE_COUNT; i++) { + verifyNodeAttesters(i, i < 3 ? groupA : groupB, i < 3 ? 'group A' : 'group B'); + } + + // Phase 2: Swap — nodes 0,1,2 get [A2,A3], nodes 3,4 get [A0,A1] + logger.info('Phase 2: Swapping all attesters'); + for (let i = 0; i < NODE_COUNT; i++) { + await writeKeystoreForNode(i, i < 3 ? groupB : groupA); + await haNodeServices[i].reloadKeystore(); + } + for (let i = 0; i < NODE_COUNT; i++) { + verifyNodeAttesters(i, i < 3 ? groupB : groupA, i < 3 ? 'group B (swapped)' : 'group A (swapped)'); + } + + const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); + const receipt = await deployer.deploy(ownerAddress, ownerAddress, 201).send({ + from: ownerAddress, + contractAddressSalt: new Fr(201), + wait: { returnReceipt: true }, + }); + expect(receipt.receipt.blockNumber).toBeDefined(); + const [block] = await aztecNode.getCheckpointedBlocks(receipt.receipt.blockNumber!, 1); + const [cp] = await aztecNode.getCheckpoints(block!.checkpointNumber, 1); + const att = cp.attestations.filter(a => !a.signature.isEmpty()); + expect(att.length).toBeGreaterThanOrEqual(quorum); + logger.info(`Phase 2: block ${receipt.receipt.blockNumber}, ${att.length} attestations (quorum ${quorum})`); + } finally { + // Restore each node's saved initial keystore so subsequent tests see original state + for (let i = 0; i < NODE_COUNT; i++) { + await writeFile(join(haKeystoreDirs[i], 'keystore.json'), initialKeystoreJsons[i]); + await haNodeServices[i].reloadKeystore(); + } + } + }); + +>>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) // NOTE: this test needs to run last it('should distribute work across multiple HA nodes', async () => { logger.info('Testing HA resilience by killing nodes after they produce blocks'); @@ -519,8 +596,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, ownerAddress, i + 100).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(i + 100)), - skipClassPublication: true, - skipInstancePublication: true, wait: { returnReceipt: true }, }); diff --git a/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts b/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts index ff0bf8f94e9b..c7b9a3be05c4 100644 --- a/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts +++ b/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts @@ -1,7 +1,7 @@ import type { AztecNodeConfig } from '@aztec/aztec-node'; import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; import { NO_WAIT, waitForProven } from '@aztec/aztec.js/contracts'; -import { ContractDeployer } from '@aztec/aztec.js/deployment'; +import { ContractDeployer, publishContractClass } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import { type AztecNode, waitForTx } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; @@ -314,6 +314,9 @@ describe('e2e_multi_validator_node', () => { config.ethereumSlotDuration * 3, 1, ); + + const publishClass = await publishContractClass(wallet, StatefulTestContractArtifact); + await publishClass.send({ from: ownerAddress }); }); afterEach(async () => { @@ -327,7 +330,6 @@ describe('e2e_multi_validator_node', () => { from: ownerAddress, contractAddressSalt, skipClassPublication: true, - skipInstancePublication: true, wait: NO_WAIT, }); }; diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts index 23b17b5d98f6..6dddbf7ced61 100644 --- a/yarn-project/end-to-end/src/e2e_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts @@ -138,8 +138,6 @@ describe('e2e_block_building', () => { const options: DeployOptions = { from: ownerAddress, contractAddressSalt: new Fr(BigInt(i + 1)), - skipClassPublication: true, - skipInstancePublication: true, }; const instance = await methods[i].getInstance(options); addresses.push(instance.address); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts index ed7cfba703fb..cd28a3568e55 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -6,6 +6,7 @@ import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { CounterContract } from '@aztec/noir-test-contracts.js/Counter'; +import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest'; import { NoConstructorContract } from '@aztec/noir-test-contracts.js/NoConstructor'; import { StatefulTestContract } from '@aztec/noir-test-contracts.js/StatefulTest'; import { GasFees } from '@aztec/stdlib/gas'; @@ -99,6 +100,31 @@ describe('e2e_deploy_contract deploy method', () => { expect((await contract.methods.summed_values(owner).simulate({ from: defaultAccountAddress })).result).toEqual(30n); }); + // The public init nullifier is emitted at the end of the initializer. If it were emitted at the beginning, + // the contract would appear initialized while the initializer body is still running, allowing external callers + // to interact with a half-initialized contract. As a consequence, any public calls the initializer enqueues + // run before the nullifier exists and cannot pass init checks. + it('refuses to self-call an init-checked function during public initialization', async () => { + const owner = defaultAccountAddress; + await expect( + InitTestContract.deployWithOpts( + { wallet, method: 'public_initializer_self_calling_init_checked' }, + owner, + 42, + ).simulate({ from: defaultAccountAddress }), + ).rejects.toThrow(/Not initialized/); + }); + + // Private functions execute before public functions, so the init check in create_note fails + // because the public initializer hasn't emitted the private initialization nullifier yet. + it('refuses to call a private init-checked function in same tx as public initialization', async () => { + const owner = defaultAccountAddress; + const deployMethod = StatefulTestContract.deployWithOpts({ wallet, method: 'public_constructor' }, owner, 42); + const contract = await deployMethod.register(); + const batch = new BatchCall(wallet, [deployMethod, contract.methods.create_note(owner, 10)]); + await expect(batch.send({ from: defaultAccountAddress })).rejects.toThrow(/Cannot find the leaf for nullifier/); + }); + it('deploys a contract with a default initializer not named constructor', async () => { logger.debug(`Deploying contract with a default initializer named initialize`); const opts = { skipClassPublication: true, skipInstancePublication: true, from: defaultAccountAddress }; diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts index 9d16aeb08bb0..ae2c526cdbce 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts @@ -97,8 +97,6 @@ describe('e2e_deploy_contract legacy', () => { const badDeploy = new ContractDeployer(artifact, wallet).deploy(AztecAddress.ZERO, ...initArgs); const firstOpts: DeployOptions = { - skipClassPublication: true, - skipInstancePublication: true, from: defaultAccountAddress, }; const secondOpts: DeployOptions = { diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts index a6814602e130..5dbe52080cbd 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts @@ -1,16 +1,19 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { BatchCall } from '@aztec/aztec.js/contracts'; +import { BatchCall, getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; +import { publishContractClass, publishInstance } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; +import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest'; import { NoConstructorContract } from '@aztec/noir-test-contracts.js/NoConstructor'; -import { StatefulTestContract } from '@aztec/noir-test-contracts.js/StatefulTest'; -import { TestContract } from '@aztec/noir-test-contracts.js/Test'; +import { PrivateInitTestContract } from '@aztec/noir-test-contracts.js/PrivateInitTest'; import { siloNullifier } from '@aztec/stdlib/hash'; import { TX_ERROR_EXISTING_NULLIFIER } from '@aztec/stdlib/tx'; import type { TestWallet } from '../test-wallet/test_wallet.js'; -import { DeployTest, type StatefulContractCtorArgs } from './deploy_test.js'; +import { DeployTest } from './deploy_test.js'; + +type InitTestCtorArgs = Parameters; describe('e2e_deploy_contract private initialization', () => { const t = new DeployTest('private initialization'); @@ -22,6 +25,7 @@ describe('e2e_deploy_contract private initialization', () => { beforeAll(async () => { ({ logger, wallet, aztecNode, defaultAccountAddress } = await t.setup()); + await publishContractClass(wallet, InitTestContract.artifact).then(c => c.send({ from: defaultAccountAddress })); }); afterAll(() => t.teardown()); @@ -30,8 +34,13 @@ describe('e2e_deploy_contract private initialization', () => { // Requires registering the contract artifact and instance locally in the pxe. // The function has a noinitcheck flag so it can be called without initialization. it('executes a noinitcheck function in an uninitialized contract', async () => { - const contract = await t.registerContract(wallet, TestContract); - const { receipt } = await contract.methods.emit_nullifier(10).send({ from: defaultAccountAddress }); + const contract = await t.registerContract(wallet, PrivateInitTestContract, { + initArgs: [0], + constructorName: 'initialize', + }); + const { receipt } = await contract.methods + .private_no_init_check_emit_nullifier(10) + .send({ from: defaultAccountAddress }); const txEffects = await aztecNode.getTxEffect(receipt.txHash); const expected = await siloNullifier(contract.address, new Fr(10)); @@ -52,69 +61,153 @@ describe('e2e_deploy_contract private initialization', () => { ).resolves.toEqual(expect.objectContaining({ result: true })); }); - // Tests privately initializing an undeployed contract. Also requires pxe registration in advance. + // Tests privately initializing an undeployed contract, then calling init-checked functions. it('privately initializes an undeployed contract from an account contract', async () => { - const owner = (await wallet.createAccount()).address; - const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); + const contract = await t.registerContract(wallet, PrivateInitTestContract, { + initArgs: [42], + constructorName: 'initialize', + }); logger.info(`Calling the constructor for ${contract.address}`); - await contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress }); + await contract.methods.initialize(42).send({ from: defaultAccountAddress }); logger.info(`Checking if the constructor was run for ${contract.address}`); - expect((await contract.methods.summed_values(owner).simulate({ from: owner })).result).toEqual(42n); + expect( + (await contract.methods.utility_read_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) + .result, + ).toEqual(42n); logger.info(`Calling a private function that requires initialization on ${contract.address}`); - await contract.methods.create_note(owner, 10).send({ from: defaultAccountAddress }); - expect((await contract.methods.summed_values(owner).simulate({ from: owner })).result).toEqual(52n); + await contract.methods.private_init_check_write_value(43).send({ from: defaultAccountAddress }); + expect( + (await contract.methods.utility_read_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) + .result, + ).toEqual(43n); }); // Tests privately initializing multiple undeployed contracts on the same tx through an account contract. it('initializes multiple undeployed contracts in a single tx', async () => { - const owner = (await wallet.createAccount()).address; - const initArgs: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]); const contracts = await Promise.all( - initArgs.map(initArgs => t.registerContract(wallet, StatefulTestContract, { initArgs })), + [42, 52].map(value => + t.registerContract(wallet, PrivateInitTestContract, { initArgs: [value], constructorName: 'initialize' }), + ), ); - const calls = contracts.map((c, i) => c.methods.constructor(...initArgs[i])); + const calls = [42, 52].map((value, i) => contracts[i].methods.initialize(value)); await new BatchCall(wallet, calls).send({ from: defaultAccountAddress }); - expect((await contracts[0].methods.summed_values(owner).simulate({ from: owner })).result).toEqual(42n); - expect((await contracts[1].methods.summed_values(owner).simulate({ from: owner })).result).toEqual(52n); + expect( + (await contracts[0].methods.utility_read_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) + .result, + ).toEqual(42n); + expect( + (await contracts[1].methods.utility_read_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) + .result, + ).toEqual(52n); }); it('initializes and calls a private function in a single tx', async () => { - const owner = (await wallet.createAccount()).address; - const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); + const contract = await t.registerContract(wallet, PrivateInitTestContract, { + initArgs: [42], + constructorName: 'initialize', + }); const batch = new BatchCall(wallet, [ - contract.methods.constructor(...initArgs), - contract.methods.create_note(owner, 10), + contract.methods.initialize(42), + contract.methods.private_init_check_write_value(43), ]); logger.info(`Executing constructor and private function in batch at ${contract.address}`); await batch.send({ from: defaultAccountAddress }); - expect((await contract.methods.summed_values(owner).simulate({ from: owner })).result).toEqual(52n); + expect( + (await contract.methods.utility_read_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) + .result, + ).toEqual(43n); }); it('refuses to initialize a contract twice', async () => { - const owner = (await wallet.createAccount()).address; - const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); - await contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress }); - await expect(contract.methods.constructor(...initArgs).send({ from: defaultAccountAddress })).rejects.toThrow( + const contract = await t.registerContract(wallet, PrivateInitTestContract, { + initArgs: [42], + constructorName: 'initialize', + }); + await contract.methods.initialize(42).send({ from: defaultAccountAddress }); + await expect(contract.methods.initialize(42).send({ from: defaultAccountAddress })).rejects.toThrow( TX_ERROR_EXISTING_NULLIFIER, ); }); it('refuses to call a private function that requires initialization', async () => { const owner = (await wallet.createAccount()).address; - const initArgs: StatefulContractCtorArgs = [owner, 42]; - const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs }); - // TODO(@spalladino): It'd be nicer to be able to fail the assert with a more descriptive message. - await expect(contract.methods.create_note(owner, 10).send({ from: defaultAccountAddress })).rejects.toThrow( + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, InitTestContract, { initArgs }); + // TODO(#14894): It'd be nicer to be able to fail the assert with a more descriptive message. + await expect(contract.methods.priv_init_check(owner, 10).send({ from: defaultAccountAddress })).rejects.toThrow( /Cannot find the leaf for nullifier/i, ); }); + // A public call enqueued before the private constructor should fail the init check, even though the + // private constructor emits the init nullifier in the same tx. + it('refuses to call a public function enqueued before private initialization in same tx', async () => { + const { contract, initArgs } = await deployUninitialized(); + const owner = defaultAccountAddress; + const batch = new BatchCall(wallet, [ + contract.methods.pub_init_check(owner, 84), + contract.methods.constructor(...initArgs), + ]); + await expect(batch.simulate({ from: defaultAccountAddress })).rejects.toThrow(/Not initialized/); + }); + + it('allows calling a public function enqueued after private initialization in same tx', async () => { + const { contract, initArgs } = await deployUninitialized(); + const owner = defaultAccountAddress; + const batch = new BatchCall(wallet, [ + contract.methods.constructor(...initArgs), + contract.methods.pub_init_check(owner, 84), + ]); + await batch.send({ from: defaultAccountAddress }); + expect((await contract.methods.pub_no_init_check(owner).simulate({ from: defaultAccountAddress })).result).toEqual( + 84n, + ); + }); + + it('allows self-calling a noinitcheck function from a private initializer', async () => { + const owner = (await wallet.createAccount()).address; + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await registerAndPublishContract(initArgs, { + constructorName: 'initializer_self_calling_private_not_init_checked', + }); + await contract.methods + .initializer_self_calling_private_not_init_checked(...initArgs) + .send({ from: defaultAccountAddress }); + }); + + it('allows calling an only_self function from a private initializer', async () => { + const owner = (await wallet.createAccount()).address; + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await registerAndPublishContract(initArgs, { + constructorName: 'initializer_calling_only_self', + }); + await contract.methods.initializer_calling_only_self(...initArgs).send({ from: defaultAccountAddress }); + }); + + it('refuses to self-call an init-checked function during private initialization', async () => { + const owner = defaultAccountAddress; + const initArgs: InitTestCtorArgs = [owner, 42]; + const contract = await t.registerContract(wallet, InitTestContract, { + initArgs, + constructorName: 'initializer_self_calling_private_init_checked', + }); + await expect( + contract.methods.initializer_self_calling_private_init_checked(...initArgs).send({ from: defaultAccountAddress }), + ).rejects.toThrow(/Cannot find the leaf for nullifier/); + }); + + it('refuses to run an enqueued public init-checked self-call from private initialization', async () => { + const { contract, initArgs } = await deployUninitialized({ + constructorName: 'initializer_enqueuing_public_init_checked', + }); + await expect( + contract.methods.initializer_enqueuing_public_init_checked(...initArgs).simulate({ from: defaultAccountAddress }), + ).rejects.toThrow(/Not initialized/); + }); + it('refuses to initialize a contract with incorrect args', async () => { const owner = (await wallet.createAccount()).address; - const contract = await t.registerContract(wallet, StatefulTestContract, { initArgs: [owner, 42] }); + const contract = await t.registerContract(wallet, InitTestContract, { initArgs: [owner, 42] }); await expect(contract.methods.constructor(owner, 43).simulate({ from: defaultAccountAddress })).rejects.toThrow( /Initialization hash does not match/, ); @@ -122,7 +215,7 @@ describe('e2e_deploy_contract private initialization', () => { it('refuses to initialize an instance from a different deployer', async () => { const owner = (await wallet.createAccount()).address; - const contract = await t.registerContract(wallet, StatefulTestContract, { + const contract = await t.registerContract(wallet, InitTestContract, { initArgs: [owner, 42], deployer: owner, }); @@ -130,4 +223,34 @@ describe('e2e_deploy_contract private initialization', () => { /Initializer address is not the contract deployer/i, ); }); + + /** Registers a contract instance locally and publishes it on-chain (so sequencers can find public function's bytecode). */ + async function registerAndPublishContract( + initArgs: InitTestCtorArgs, + opts: { constructorName?: string; deployer?: AztecAddress } = {}, + ) { + const salt = Fr.random(); + const instance = await getContractInstanceFromInstantiationParams(InitTestContract.artifact, { + constructorArgs: initArgs, + salt, + constructorArtifact: opts.constructorName, + deployer: opts.deployer, + }); + await publishInstance(wallet, instance).send({ from: defaultAccountAddress }); + return t.registerContract(wallet, InitTestContract, { + initArgs, + salt, + constructorName: opts.constructorName, + deployer: opts.deployer, + }); + } + + /** Publishes a contract instance on-chain without initializing it. */ + async function deployUninitialized(opts: { constructorName?: string } = {}) { + const owner = defaultAccountAddress; + const initArgs: InitTestCtorArgs = [owner, 42]; + const constructorName = opts.constructorName; + const contract = await registerAndPublishContract(initArgs, { constructorName }); + return { contract, initArgs }; + } }); diff --git a/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts b/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts index 02bad7926b51..3b776cb963c4 100644 --- a/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_eoa.test.ts @@ -111,8 +111,6 @@ describe('e2e_multi_eoa', () => { const deployMethod = StatefulTestContract.deploy(wallet, defaultAccountAddress, 0); const deployMethodTx = await proveInteraction(wallet, deployMethod, { contractAddressSalt: Fr.random(), - skipClassPublication: true, - skipInstancePublication: true, from: defaultAccountAddress, }); diff --git a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts index 47ec290401f2..53060168826d 100644 --- a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts @@ -117,8 +117,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - skipClassPublication: true, - skipInstancePublication: true, wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { @@ -180,8 +178,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - skipClassPublication: true, - skipInstancePublication: true, wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { diff --git a/yarn-project/end-to-end/src/e2e_sequencer/reload_keystore.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/reload_keystore.test.ts index 667728c4056b..221c3086d924 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/reload_keystore.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/reload_keystore.test.ts @@ -133,8 +133,6 @@ describe('e2e_reload_keystore', () => { const { txHash: sentTx1 } = await deployer.deploy(ownerAddress, ownerAddress, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(1), - skipClassPublication: true, - skipInstancePublication: true, wait: NO_WAIT, }); const receipt1 = await waitForTx(aztecNode, sentTx1); @@ -200,8 +198,6 @@ describe('e2e_reload_keystore', () => { const { txHash: sentTx2 } = await deployer.deploy(ownerAddress, ownerAddress, 2).send({ from: ownerAddress, contractAddressSalt: new Fr(2), - skipClassPublication: true, - skipInstancePublication: true, wait: NO_WAIT, }); const receipt2 = await waitForTx(aztecNode, sentTx2); diff --git a/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts b/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts index 9db69b105ae2..a0c91687d866 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer_config.test.ts @@ -2,6 +2,7 @@ import { getInitialTestAccountsData } from '@aztec/accounts/testing'; import type { AztecNode } from '@aztec/aztec.js/node'; import { TxReceipt } from '@aztec/aztec.js/tx'; import { Bot, type BotConfig, BotStore, getBotDefaultConfig } from '@aztec/bot'; +import { DEFAULT_DA_GAS_LIMIT } from '@aztec/constants'; import type { Logger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import type { SequencerClient } from '@aztec/sequencer-client'; @@ -82,7 +83,7 @@ describe('e2e_sequencer_config', () => { expect(totalManaUsed).toBeGreaterThan(0n); bot.updateConfig({ l2GasLimit: Number(totalManaUsed), - daGasLimit: Number(totalManaUsed), + daGasLimit: DEFAULT_DA_GAS_LIMIT, }); // Set the maxL2BlockGas to the total mana used diff --git a/yarn-project/end-to-end/src/e2e_simple.test.ts b/yarn-project/end-to-end/src/e2e_simple.test.ts index 1dbfe5861776..e38df666d579 100644 --- a/yarn-project/end-to-end/src/e2e_simple.test.ts +++ b/yarn-project/end-to-end/src/e2e_simple.test.ts @@ -75,8 +75,6 @@ describe('e2e_simple', () => { const { receipt: txReceipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - skipClassPublication: true, - skipInstancePublication: true, wait: { returnReceipt: true }, }); await waitForProven(aztecNode, txReceipt, { From cfe7cf7bf52dc857e867c90708e0405711d96f70 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Thu, 12 Mar 2026 12:24:01 +0000 Subject: [PATCH 3/7] fix: resolve cherry-pick conflicts for PR #20775 Resolved conflicts in: - docs/docs-developers/docs/resources/migration_notes.md - noir-projects/aztec-nr/aztec/src/macros/aztec.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants.nr - noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr - yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts --- .../docs/resources/migration_notes.md | 14 ---- .../aztec-nr/aztec/src/macros/aztec.nr | 23 +----- .../crates/types/src/constants.nr | 4 - .../crates/types/src/constants_tests.nr | 6 +- .../src/composed/ha/e2e_ha_full.test.ts | 79 ------------------- 5 files changed, 2 insertions(+), 124 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index ab9c6227be70..e8267807fe70 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,8 +9,6 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD -<<<<<<< HEAD -======= ### Two separate init nullifiers for private and public Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain: @@ -35,18 +33,6 @@ Contract initialization now emits two separate nullifiers instead of one: a **pr - skipClassPublication: true, }).deployed(); ``` - -### [Aztec.nr] Made `compute_note_hash_for_nullification` unconstrained - -This function shouldn't have been constrained in the first place, as constrained computation of `HintedNote` nullifiers is dangerous (constrained computation of nullifiers can be performed only on the `ConfirmedNote` type). If you were calling this from a constrained function, consider using `compute_confirmed_note_hash_for_nullification` instead. Unconstrained usage is safe. - -### [Aztec.nr] Changes to standard note hash computation - -Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation. - -This change requires no input from your side unless you were testing or relying on hardcoded note hashes. - ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) ### [Aztec.js] `getPublicEvents` now returns an object instead of an array `getPublicEvents` now returns a `GetPublicEventsResult` object with `events` and `maxLogsHit` fields instead of a plain array. This enables pagination through large result sets using the new `afterLog` filter option. diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index 7f58bce688de..21451a328ef3 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -1,33 +1,16 @@ -<<<<<<< HEAD use crate::macros::{ calls_generation::{ external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs}, internal_functions::generate_call_internal_struct, }, dispatch::generate_public_dispatch, + emit_public_init_nullifier::generate_emit_public_init_nullifier, internals_functions_generation::{create_fn_abi_exports, process_functions}, notes::NOTES, storage::STORAGE_LAYOUT_NAME, utils::{ get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, module_has_storage, -======= -use crate::{ - macros::{ - calls_generation::{ - external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs}, - internal_functions::generate_call_internal_struct, - }, - dispatch::generate_public_dispatch, - emit_public_init_nullifier::generate_emit_public_init_nullifier, - internals_functions_generation::{create_fn_abi_exports, process_functions}, - notes::NOTES, - storage::STORAGE_LAYOUT_NAME, - utils::{ - get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, - module_has_storage, - }, ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) }, }; @@ -84,11 +67,7 @@ pub comptime fn aztec(m: Module) -> Quoted { $public_dispatch $sync_state_fn_and_abi_export $process_message_fn_and_abi_export -<<<<<<< HEAD -======= $emit_public_init_nullifier_fn_body - $offchain_receive_fn_and_abi_export ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index b5a95fc2b566..116fb2002d64 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -713,12 +713,8 @@ pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423; // TODO: consider moving these to aztec-nr pub global DOM_SEP__INITIALIZATION_NULLIFIER: u32 = 1653084894; -<<<<<<< HEAD -======= pub global DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER: u32 = 3342006647; -/// Domain separator for L1 to L2 message secret hashes. ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) pub global DOM_SEP__SECRET_HASH: u32 = 4199652938; pub global DOM_SEP__TX_NULLIFIER: u32 = 1025801951; pub global DOM_SEP__SIGNATURE_PAYLOAD: u32 = 463525807; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr index 5025b22a56cf..6a5048b6926f 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr @@ -130,11 +130,7 @@ impl HashedValueTester::new(); -======= - let mut tester = HashedValueTester::<52, 45>::new(); ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) + let mut tester = HashedValueTester::<51, 44>::new(); // ----------------- // Domain separators diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts index 391531da1583..9739b6f74bd8 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts @@ -491,85 +491,6 @@ describe('HA Full Setup', () => { logger.info('Governance voting with HA coordination and L1 verification complete'); }); -<<<<<<< HEAD -======= - it('should reload keystore via admin API and keep building blocks after swapping attesters', async () => { - logger.info('Testing reloadKeystore: swap all attesters across HA nodes'); - - const groupA = attesterAddresses.slice(0, 2); - const groupB = attesterAddresses.slice(2, 4); - - const writeKeystoreForNode = async (nodeIdx: number, attesters: string[]) => { - const ks = { - schemaVersion: 1, - validators: [ - { - attester: attesters, - feeRecipient: AztecAddress.ZERO.toString(), - coinbase: EthAddress.fromString(attesters[0]).toChecksumString(), - remoteSigner: web3SignerUrl, - publisher: [publisherAddresses[nodeIdx]], - }, - ], - }; - await writeFile(join(haKeystoreDirs[nodeIdx], 'keystore.json'), JSON.stringify(ks, null, 2)); - }; - - const verifyNodeAttesters = (nodeIdx: number, expectedAttesters: string[], label: string) => { - const vc: ValidatorClient = (haNodeServices[nodeIdx] as any).validatorClient; - const addrs = vc.getValidatorAddresses(); - expect(addrs).toHaveLength(expectedAttesters.length); - for (const expected of expectedAttesters) { - expect(addrs.some(a => a.equals(EthAddress.fromString(expected)))).toBe(true); - } - logger.info(`Node ${nodeIdx}: ${addrs.length} attesters (${label})`); - }; - - const quorum = Math.floor((COMMITTEE_SIZE * 2) / 3) + 1; - - try { - // Phase 1: Nodes 0,1,2 get attesters [A0,A1], nodes 3,4 get [A2,A3] - logger.info('Phase 1: Initial attester split'); - for (let i = 0; i < NODE_COUNT; i++) { - await writeKeystoreForNode(i, i < 3 ? groupA : groupB); - await haNodeServices[i].reloadKeystore(); - } - for (let i = 0; i < NODE_COUNT; i++) { - verifyNodeAttesters(i, i < 3 ? groupA : groupB, i < 3 ? 'group A' : 'group B'); - } - - // Phase 2: Swap — nodes 0,1,2 get [A2,A3], nodes 3,4 get [A0,A1] - logger.info('Phase 2: Swapping all attesters'); - for (let i = 0; i < NODE_COUNT; i++) { - await writeKeystoreForNode(i, i < 3 ? groupB : groupA); - await haNodeServices[i].reloadKeystore(); - } - for (let i = 0; i < NODE_COUNT; i++) { - verifyNodeAttesters(i, i < 3 ? groupB : groupA, i < 3 ? 'group B (swapped)' : 'group A (swapped)'); - } - - const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); - const receipt = await deployer.deploy(ownerAddress, ownerAddress, 201).send({ - from: ownerAddress, - contractAddressSalt: new Fr(201), - wait: { returnReceipt: true }, - }); - expect(receipt.receipt.blockNumber).toBeDefined(); - const [block] = await aztecNode.getCheckpointedBlocks(receipt.receipt.blockNumber!, 1); - const [cp] = await aztecNode.getCheckpoints(block!.checkpointNumber, 1); - const att = cp.attestations.filter(a => !a.signature.isEmpty()); - expect(att.length).toBeGreaterThanOrEqual(quorum); - logger.info(`Phase 2: block ${receipt.receipt.blockNumber}, ${att.length} attestations (quorum ${quorum})`); - } finally { - // Restore each node's saved initial keystore so subsequent tests see original state - for (let i = 0; i < NODE_COUNT; i++) { - await writeFile(join(haKeystoreDirs[i], 'keystore.json'), initialKeystoreJsons[i]); - await haNodeServices[i].reloadKeystore(); - } - } - }); - ->>>>>>> cee97a1ba4 (feat!: auto-enqueue public init nullifier for contracts with public functions (#20775)) // NOTE: this test needs to run last it('should distribute work across multiple HA nodes', async () => { logger.info('Testing HA resilience by killing nodes after they produce blocks'); From aa0ccd19bcef5354f477ddf6e342ce382948098c Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 12 Mar 2026 12:06:07 -0300 Subject: [PATCH 4/7] cherry-pick: feat(p2p): reject and evict txs with insufficient max fee per gas (#21281) Cherry-pick of 9e2d79c7299fffe2d2f165d10e668fa175033af3 with conflicts. Conflicts: - yarn-project/end-to-end/src/spartan/block_capacity.test.ts: modify/delete conflict (deleted in v4-next, modified in source) --- .../aztec-node/src/aztec-node/server.ts | 15 +- .../cli-wallet/src/utils/constants.ts | 2 +- .../client_flows/client_flows_benchmark.ts | 4 +- .../e2e_local_network_example.test.ts | 3 +- .../src/e2e_fees/account_init.test.ts | 7 +- .../end-to-end/src/e2e_fees/fees_test.ts | 6 +- .../e2e_p2p/gossip_network_no_cheat.test.ts | 7 + .../end-to-end/src/e2e_p2p/p2p_network.ts | 1 - .../end-to-end/src/fixtures/fixtures.ts | 3 + yarn-project/end-to-end/src/fixtures/setup.ts | 2 +- .../src/spartan/block_capacity.test.ts | 477 ++++++++++++++++++ .../src/spartan/n_tps_prove.test.ts | 5 +- .../end-to-end/src/test-wallet/test_wallet.ts | 5 +- yarn-project/p2p/src/client/factory.ts | 5 + .../proposal_tx_collector_worker.ts | 2 + .../mem_pools/tx_pool_v2/eviction/index.ts | 1 + ...fficient_fee_per_gas_eviction_rule.test.ts | 185 +++++++ .../insufficient_fee_per_gas_eviction_rule.ts | 65 +++ .../src/mem_pools/tx_pool_v2/interfaces.ts | 3 + .../src/mem_pools/tx_pool_v2/tx_metadata.ts | 18 +- .../tx_pool_v2/tx_pool_v2.compat.test.ts | 6 + .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 269 +++++++++- .../tx_pool_v2/tx_pool_v2_bench.test.ts | 3 + .../mem_pools/tx_pool_v2/tx_pool_v2_impl.ts | 2 + .../src/msg_validators/tx_validator/README.md | 14 +- .../tx_validator/factory.test.ts | 14 +- .../msg_validators/tx_validator/factory.ts | 4 +- .../tx_validator/gas_validator.test.ts | 46 +- .../tx_validator/gas_validator.ts | 88 ++-- .../src/test-helpers/make-test-p2p-clients.ts | 2 + .../testbench/p2p_client_testbench_worker.ts | 2 + yarn-project/stdlib/src/gas/gas_fees.ts | 5 + .../stdlib/src/tx/global_variable_builder.ts | 6 +- 33 files changed, 1199 insertions(+), 78 deletions(-) create mode 100644 yarn-project/end-to-end/src/spartan/block_capacity.test.ts create mode 100644 yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts create mode 100644 yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index da94fd8fb1c5..e89d5f3a09d9 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -330,6 +330,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems'); } + const globalVariableBuilder = new GlobalVariableBuilder({ + ...config, + rollupVersion: BigInt(config.rollupVersion), + l1GenesisTime, + slotDuration: Number(slotDuration), + }); + // create the tx pool and the p2p client, which will need the l2 block source const p2pClient = await createP2PClient( config, @@ -337,6 +344,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { proofVerifier, worldStateSynchronizer, epochCache, + globalVariableBuilder, packageVersion, dateProvider, telemetry, @@ -550,13 +558,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } } - const globalVariableBuilder = new GlobalVariableBuilder({ - ...config, - rollupVersion: BigInt(config.rollupVersion), - l1GenesisTime, - slotDuration: Number(slotDuration), - }); - const node = new AztecNodeService( config, p2pClient, diff --git a/yarn-project/cli-wallet/src/utils/constants.ts b/yarn-project/cli-wallet/src/utils/constants.ts index ab747206179a..9c472b096e82 100644 --- a/yarn-project/cli-wallet/src/utils/constants.ts +++ b/yarn-project/cli-wallet/src/utils/constants.ts @@ -1,4 +1,4 @@ -export const MIN_FEE_PADDING = 0.5; +export const MIN_FEE_PADDING = 10; export const AccountTypes = ['schnorr', 'ecdsasecp256r1', 'ecdsasecp256r1ssh', 'ecdsasecp256k1'] as const; export type AccountType = (typeof AccountTypes)[number]; diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index d0592a8132fb..a8a598439919 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -27,7 +27,7 @@ import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { GasSettings } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; -import { MNEMONIC } from '../../fixtures/fixtures.js'; +import { E2E_DEFAULT_MIN_FEE_PADDING, MNEMONIC } from '../../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, deployAccounts, setup, teardown } from '../../fixtures/setup.js'; import { mintTokensToPrivate } from '../../fixtures/token_utils.js'; import { setupSponsoredFPC } from '../../fixtures/utils.js'; @@ -376,7 +376,7 @@ export class ClientFlowsBenchmark { public async getPrivateFPCPaymentMethodForWallet(wallet: Wallet, sender: AztecAddress) { // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await this.aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); return new PrivateFeePaymentMethod(this.bananaFPC.address, sender, wallet, gasSettings); } diff --git a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts index 7bd8d64e0fad..c311e1e225c5 100644 --- a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts @@ -16,6 +16,7 @@ import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/test import { format } from 'util'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import { deployToken, mintTokensToPrivate } from '../fixtures/token_utils.js'; import { TestWallet } from '../test-wallet/test_wallet.js'; @@ -185,7 +186,7 @@ describe('e2e_local_network_example', () => { // docs:start:private_fpc_payment // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await node.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await node.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPCAddress, alice, wallet, gasSettings); const { receipt: receiptForAlice } = await bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index 1323bd1ba00a..f1e2d3a0f9f5 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -16,6 +16,7 @@ import { GasSettings } from '@aztec/stdlib/gas'; import { jest } from '@jest/globals'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; import { FeesTest } from './fees_test.js'; @@ -115,7 +116,7 @@ describe('e2e_fees account_init', () => { // Bob deploys his account through the private FPC // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ @@ -144,7 +145,7 @@ describe('e2e_fees account_init', () => { // The public fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const paymentMethod = new PublicFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ @@ -201,7 +202,7 @@ describe('e2e_fees account_init', () => { expect(aliceBalanceAfter).toBe(aliceBalanceBefore - tx.transactionFee!); // bob can now use his wallet for sending txs - const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(1.5); + const maxFeesPerGas = (await aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING); const gasSettings = GasSettings.default({ maxFeesPerGas }); const bobPaymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); await bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index fca622ac6bff..d54fc061b941 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -23,7 +23,7 @@ import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { getContract } from 'viem'; -import { MNEMONIC } from '../fixtures/fixtures.js'; +import { E2E_DEFAULT_MIN_FEE_PADDING, MNEMONIC } from '../fixtures/fixtures.js'; import { type EndToEndContext, type SetupOptions, @@ -193,7 +193,9 @@ export class FeesTest { this.wallet = this.context.wallet; this.aztecNode = this.context.aztecNodeService; this.aztecNodeAdmin = this.context.aztecNodeService; - this.gasSettings = GasSettings.default({ maxFeesPerGas: (await this.aztecNode.getCurrentMinFees()).mul(2) }); + this.gasSettings = GasSettings.default({ + maxFeesPerGas: (await this.aztecNode.getCurrentMinFees()).mul(E2E_DEFAULT_MIN_FEE_PADDING), + }); this.cheatCodes = this.context.cheatCodes; this.accounts = deployedAccounts.map(a => a.address); this.accounts.forEach((a, i) => this.logger.verbose(`Account ${i} address: ${a}`)); diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts index 40abce5a9573..54e467eb0ff2 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts @@ -203,6 +203,13 @@ describe('e2e_p2p_network', () => { // blocks without them (since targetCommitteeSize is set to the number of nodes) await t.setupAccount(); + // Wait for the next L1 block so that all nodes' getCurrentMinFees() caches are + // refreshed after the first L2 checkpoint is published. Without this, some wallets + // may estimate fees based on pre-checkpoint values (very low due to fee decay), + // while receiving nodes already see the post-checkpoint fees (much higher). + const ethereumSlotDuration = t.ctx.aztecNodeConfig.ethereumSlotDuration ?? 4; + await sleep((ethereumSlotDuration + 1) * 1000); + t.logger.info('Submitting transactions'); for (const node of nodes) { const txs = await submitTransactions(t.logger, node, NUM_TXS_PER_NODE, t.fundedAccount); diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index 1f2120f28177..f53fc7c578f0 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -129,7 +129,6 @@ export class P2PNetworkTest { metricsPort: metricsPort, numberOfInitialFundedAccounts: 2, startProverNode, - walletMinFeePadding: 2.0, }; this.deployL1ContractsArgs = { diff --git a/yarn-project/end-to-end/src/fixtures/fixtures.ts b/yarn-project/end-to-end/src/fixtures/fixtures.ts index 6b2002c6ffcd..d7cab015e53b 100644 --- a/yarn-project/end-to-end/src/fixtures/fixtures.ts +++ b/yarn-project/end-to-end/src/fixtures/fixtures.ts @@ -1,5 +1,8 @@ export const METRICS_PORT = 4318; +/** Default fee multiplier applied to getCurrentMinFees in e2e tests to cover fee decay between blocks. */ +export const E2E_DEFAULT_MIN_FEE_PADDING = 15; + export const shouldCollectMetrics = () => { if (process.env.COLLECT_METRICS) { return METRICS_PORT; diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index fb9ab8d9d88f..870a0efcce40 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -195,7 +195,7 @@ export type SetupOptions = { skipAccountDeployment?: boolean; /** L1 contracts deployment arguments. */ l1ContractsArgs?: Partial; - /** Wallet minimum fee padding multiplier (defaults to 0.5, which is 50% padding). */ + /** Wallet minimum fee padding multiplier */ walletMinFeePadding?: number; } & Partial; diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts new file mode 100644 index 000000000000..10621320ed1a --- /dev/null +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -0,0 +1,477 @@ +import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { type ContractFunctionInteraction, NO_WAIT, toSendOptions } from '@aztec/aztec.js/contracts'; +import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; +import { type AztecNode, createAztecNodeClient, waitForTx } from '@aztec/aztec.js/node'; +import { AccountManager } from '@aztec/aztec.js/wallet'; +import { asyncPool } from '@aztec/foundation/async-pool'; +import { BlockNumber } from '@aztec/foundation/branded-types'; +import { times } from '@aztec/foundation/collection'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import { createLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; +import { TokenContract } from '@aztec/noir-contracts.js/Token'; +import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; +import { GasFees } from '@aztec/stdlib/gas'; +import { deriveSigningKey } from '@aztec/stdlib/keys'; +import { Tx } from '@aztec/stdlib/tx'; + +import { jest } from '@jest/globals'; +import { mkdir, writeFile } from 'fs/promises'; +import { dirname } from 'path'; + +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; +import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js'; +import type { WorkerWallet } from '../test-wallet/worker_wallet.js'; +import { type WorkerWalletWrapper, createWorkerWalletClient } from './setup_test_wallets.js'; +import { + fetchBlockBuiltLogs, + getExternalIP, + getSequencersConfig, + setupEnvironment, + updateSequencersConfig, +} from './utils.js'; + +const config = setupEnvironment(process.env); +const txRealProofs = config.REAL_VERIFIER || config.DEBUG_FORCE_TX_PROOF_VERIFICATION; + +const BENCH_TESTS = [ + ['noop', 100], + ['noop_pub', 100], + ['emit_nullifiers', 100], + ['emit_note_hashes', 100], + ['emit_l2_to_l1_msgs', 100], + ['emit_private_logs', 88], // we run out of blob space + ['emit_contract_class_log', 8], +] as const; + +const TOKEN_TESTS = [ + // intentional comment - for file fomatting + ['transfer_in_public', 100], +] as const; + +const maxTxs = Math.max(...[...BENCH_TESTS, ...TOKEN_TESTS].map(t => t[1])); +const NUM_WALLETS = txRealProofs ? Math.min(10, maxTxs) : 1; + +describe('block capacity benchmark', () => { + jest.setTimeout(60 * 60 * 1000); // 60 minutes + + const logger = createLogger('e2e:spartan-test:block-capacity'); + + let testWallets: WorkerWalletWrapper[]; + let wallets: WorkerWallet[]; + let accountAddresses: AztecAddress[]; + let aztecNode: AztecNode; + let originalSequencerConfig: Awaited> | undefined; + const benchmarkData: Array<{ name: string; unit: string; value: number }> = []; + + beforeAll(async () => { + logger.info('Setting up block capacity benchmark', { + numWallets: NUM_WALLETS, + txRealProofs, + namespace: config.NAMESPACE, + }); + + await updateSequencersConfig(config, { minTxsPerBlock: 0 }); + + const rpcIP = await getExternalIP(config.NAMESPACE, 'rpc-aztec-node'); + const rpcUrl = `http://${rpcIP}:8080`; + aztecNode = createAztecNodeClient(rpcUrl); + + // Wait for node to be ready + await retryUntil(async () => await aztecNode.isReady(), 'node ready', 120, 1); + logger.info('Node is ready'); + + // Save original sequencer config for restoration + originalSequencerConfig = await getSequencersConfig(config); + logger.info('Saved original sequencer config', { + minTxsPerBlock: originalSequencerConfig[0]?.minTxsPerBlock, + }); + + // Create WorkerWallets in parallel + logger.info(`Creating ${NUM_WALLETS} worker wallet(s)...`); + testWallets = await Promise.all( + Array.from({ length: NUM_WALLETS }, (_, i) => { + logger.info(`Creating wallet ${i + 1}/${NUM_WALLETS}`); + return createWorkerWalletClient(rpcUrl, txRealProofs, logger); + }), + ); + wallets = testWallets.map(tw => tw.wallet); + + // Register FPC and create/deploy accounts in parallel + const fpcAddress = await getSponsoredFPCAddress(); + const sponsor = new SponsoredFeePaymentMethod(fpcAddress); + accountAddresses = await Promise.all( + wallets.map(async wallet => { + const secret = Fr.random(); + const salt = Fr.random(); + const address = await wallet.registerAccount(secret, salt); + await registerSponsoredFPC(wallet); + const manager = await AccountManager.create( + wallet, + secret, + new SchnorrAccountContract(deriveSigningKey(secret)), + salt, + ); + const deployMethod = await manager.getDeployMethod(); + await deployMethod.send({ + from: AztecAddress.ZERO, + fee: { paymentMethod: sponsor }, + wait: { timeout: 2400 }, + }); + logger.info(`Account deployed at ${address}`); + return address; + }), + ); + }); + + afterAll(async () => { + // Write benchmark output if configured + if (process.env.BENCH_OUTPUT && benchmarkData.length > 0) { + const scenario = process.env.BENCH_SCENARIO?.trim(); + const finalData = scenario + ? benchmarkData.map(e => ({ ...e, name: `scenario/${scenario}/${e.name}` })) + : benchmarkData; + await mkdir(dirname(process.env.BENCH_OUTPUT), { recursive: true }); + await writeFile(process.env.BENCH_OUTPUT, JSON.stringify(finalData)); + logger.info('Wrote benchmark output', { path: process.env.BENCH_OUTPUT, entries: finalData.length }); + } + + // Restore original sequencer config + if (originalSequencerConfig?.[0]) { + logger.info('Restoring original sequencer config'); + await updateSequencersConfig(config, originalSequencerConfig[0]); + } + + if (testWallets) { + for (const tw of testWallets) { + await tw.cleanup(); + } + } + + logger.info('Cleanup complete'); + }); + + /** Creates and proves a single tx from a contract interaction. */ + async function createProvableTx( + wallet: WorkerWallet, + accountAddress: AztecAddress, + interaction: ContractFunctionInteraction, + ): Promise { + const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); + const options = { + from: accountAddress, + fee: { paymentMethod: sponsor, gasSettings: { maxPriorityFeesPerGas: GasFees.empty() } }, + }; + const execPayload = await interaction.request(options); + return wallet.proveTx(execPayload, toSendOptions(options)); + } + + /** Pre-proves TX_COUNT txs, either in parallel batches or by cloning a prototype. */ + async function proveOrCloneTxs( + txCount: number, + createPrototypeFn: (wallet: WorkerWallet, accountAddress: AztecAddress) => Promise, + ): Promise { + const txs: Tx[] = []; + if (txRealProofs) { + for (let i = 0; i < txCount; i += NUM_WALLETS) { + const batchSize = Math.min(NUM_WALLETS, txCount - txs.length); + const batchTxs = await Promise.all(times(batchSize, j => createPrototypeFn(wallets[j], accountAddresses[j]))); + txs.push(...batchTxs); + logger.info(`Proved ${txs.length}/${txCount} txs`); + } + } else { + const prototypeTx = await createPrototypeFn(wallets[0], accountAddresses[0]); + logger.info('Prototype tx proved, cloning...'); + for (let i = 0; i < txCount; i++) { + txs.push(await cloneTx(prototypeTx, aztecNode)); + if ((i + 1) % 10 === 0 || i === txCount - 1) { + logger.info(`Cloned ${i + 1}/${txCount} txs`); + } + } + } + return txs; + } + + /** Floods the mempool with pre-proven txs and measures block capacity. */ + async function floodAndMeasure( + label: string, + provenTxs: Tx[], + ): Promise<{ blockTxCounts: { blockNumber: number; txCount: number }[]; enabledAt: string }> { + const epochDurationSec = 2 * config.AZTEC_EPOCH_DURATION * config.AZTEC_SLOT_DURATION; // wait for up to two epochs (these are shorter epochs than standard) + const txCount = provenTxs.length; + + // 0. wait for the mempool to clear + await retryUntil( + async () => { + const pendingTxs = await aztecNode.getPendingTxCount(); + + if (pendingTxs > 0) { + logger.info(`Waiting for mempool to clear before sending test txs: ${pendingTxs} pending txs left.`); + return false; + } else { + return true; + } + }, + 'clear pending txs', + epochDurationSec, + 1, + ); + + // 1. Disable block building by setting minTxsPerBlock extremely high + logger.info(`[${label}] Disabling block building`); + await updateSequencersConfig(config, { minTxsPerBlock: 999_999_999 }); + await retryUntil( + async () => { + const configs = await getSequencersConfig(config); + return configs.every(c => c.minTxsPerBlock === 999_999_999); + }, + 'disable block building', + 60, + 1, + ); + logger.info(`[${label}] Block building disabled`); + + const blockBeforeFlood = await aztecNode.getBlockNumber(); + logger.info(`[${label}] Block number before flood`, { blockBeforeFlood }); + + // 2. Send all pre-proven txs to mempool + logger.info(`[${label}] Sending ${provenTxs.length} pre-proven txs to mempool`); + const sendStartMs = Date.now(); + + let sentCount = 0; + const txSize = provenTxs[0].toBuffer().length; + logger.info(`Tx size: ${(txSize / 1024 / 1024).toFixed(2)}MB (${txSize} bytes)`); + // dynamically adjust how many txs we can send to stay below 1MB + await asyncPool(Math.max(1, Math.floor((0.5 * 1024 * 1024) / txSize)), provenTxs, async tx => { + await aztecNode.sendTx(tx); + sentCount++; + if (sentCount % 10 === 0 || sentCount === provenTxs.length) { + logger.info(`[${label}] Sent ${sentCount}/${provenTxs.length} txs`); + } + }); + + const sendDurationMs = Date.now() - sendStartMs; + logger.info(`[${label}] All ${provenTxs.length} txs sent to mempool`, { sendDurationMs }); + + // 3. Re-enable block building + const enabledAt = new Date().toISOString(); + logger.info(`[${label}] Re-enabling block building`); + await updateSequencersConfig(config, { minTxsPerBlock: 1, enforceTimeTable: true }); + await retryUntil( + async () => { + const configs = await getSequencersConfig(config); + return configs.every(c => c.minTxsPerBlock === 1); + }, + 'enable block building', + 60, + 1, + ); + logger.info(`[${label}] Block building re-enabled`); + + // 4. Wait for blocks and observe inclusion + let totalTxsMined = 0; + const blockTxCounts: { blockNumber: number; txCount: number }[] = []; + + await retryUntil( + async () => { + const currentBlock = await aztecNode.getBlockNumber(); + for (let bn = blockBeforeFlood + 1; bn <= currentBlock; bn++) { + if (blockTxCounts.some(b => b.blockNumber === bn)) { + continue; + } + const block = await aztecNode.getBlock(BlockNumber(bn)); + if (block) { + const txCount = block.body.txEffects.length; + blockTxCounts.push({ blockNumber: bn, txCount }); + totalTxsMined += txCount; + logger.info(`[${label}] Block ${bn}: ${txCount} txs (total mined: ${totalTxsMined}/${txCount})`); + } + } + return totalTxsMined >= txCount; + }, + 'all txs mined', + epochDurationSec, + 1, + ); + + // Log summary + logger.info(`=== Block Capacity Benchmark Results (${label}) ===`); + logger.info(`Total txs sent: ${txCount}`); + logger.info(`Total txs mined: ${totalTxsMined}`); + logger.info(`Blocks produced: ${blockTxCounts.length}`); + for (const { blockNumber, txCount } of blockTxCounts) { + logger.info(` Block ${blockNumber}: ${txCount} txs`); + } + + if (blockTxCounts.length > 0) { + const maxTxsInBlock = Math.max(...blockTxCounts.map(b => b.txCount)); + const avgTxsPerBlock = totalTxsMined / blockTxCounts.length; + logger.info(`Max txs in a single block: ${maxTxsInBlock}`); + logger.info(`Avg txs per block: ${avgTxsPerBlock.toFixed(1)}`); + } + + expect(totalTxsMined).toBeGreaterThanOrEqual(txCount); + + return { blockTxCounts, enabledAt }; + } + + /** Fetches block-built stats from sequencer logs and records benchmark metrics for each block. */ + async function recordBlockBuiltMetrics( + label: string, + blockTxCounts: { blockNumber: number; txCount: number }[], + enabledAt: string, + ): Promise { + const blockNumbers = new Set(blockTxCounts.map(b => b.blockNumber)); + const entries = await fetchBlockBuiltLogs(config.NAMESPACE, enabledAt, blockNumbers, logger); + + if (entries.length === 0) { + logger.warn(`[${label}] No block-built log entries found, skipping benchmark metrics`); + return; + } + + // Record metrics for each block (entries are sorted by blockNumber ascending) + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const prefix = `block_capacity/${label}/block_${i}`; + benchmarkData.push( + { name: `${prefix}/duration`, unit: 'ms', value: entry.duration }, + { name: `${prefix}/tx_count`, unit: 'count', value: entry.txCount }, + { name: `${prefix}/mana_per_sec`, unit: 'mana/s', value: entry.manaPerSec }, + { name: `${prefix}/public_process_duration`, unit: 'ms', value: entry.publicProcessDuration }, + { name: `${prefix}/private_log_count`, unit: 'count', value: entry.privateLogCount }, + { name: `${prefix}/public_log_count`, unit: 'count', value: entry.publicLogCount }, + { name: `${prefix}/contract_class_log_count`, unit: 'count', value: entry.contractClassLogCount }, + { name: `${prefix}/contract_class_log_size`, unit: 'fields', value: entry.contractClassLogSize }, + ); + logger.info(`[${label}] Recorded benchmark metrics from block ${entry.blockNumber} (index ${i})`, entry); + } + + benchmarkData.push({ + name: `block_capacity/${label}/blocks_produced`, + unit: 'count', + value: entries.length, + }); + } + + describe('Benchmark contract', () => { + let benchmarkContract: BenchmarkingContract; + + beforeAll(async () => { + const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); + // Deploy BenchmarkingContract using the first wallet + logger.info('Deploying benchmark contract...'); + ({ contract: benchmarkContract } = await BenchmarkingContract.deploy(wallets[0]).send({ + from: accountAddresses[0], + fee: { paymentMethod: sponsor }, + })); + logger.info('BenchmarkingContract deployed', { address: benchmarkContract.address.toString() }); + + // Register benchmark contract with all other wallets + const benchMetadata = await wallets[0].getContractMetadata(benchmarkContract.address); + await Promise.all( + wallets.slice(1).map(wallet => wallet.registerContract(benchMetadata.instance!, BenchmarkingContract.artifact)), + ); + logger.info('Benchmark contract registered with all wallets'); + }); + it.each(BENCH_TESTS)('measures block capacity with %s', async (fnName, txCount) => { + logger.info(`Pre-proving ${txCount} ${fnName} txs...`); + const txs = await proveOrCloneTxs(txCount, (wallet, addr) => { + const contract = BenchmarkingContract.at(benchmarkContract.address, wallet); + return createProvableTx(wallet, addr, contract.methods[fnName]()); + }); + logger.info(`All ${txCount} ${fnName} txs pre-proven`); + const { blockTxCounts, enabledAt } = await floodAndMeasure(fnName, txs); + await recordBlockBuiltMetrics(fnName, blockTxCounts, enabledAt); + }); + }); + + describe('Token contract', () => { + let tokenContract: TokenContract; + + beforeAll(async () => { + const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); + // Deploy TokenContract using the first wallet + logger.info('Deploying token contract...'); + ({ contract: tokenContract } = await TokenContract.deploy( + wallets[0], + accountAddresses[0], + 'USDC', + 'USD', + 18n, + ).send({ + from: accountAddresses[0], + fee: { paymentMethod: sponsor }, + wait: { timeout: 600 }, + })); + logger.info('TokenContract deployed', { address: tokenContract.address.toString() }); + + // Register token contract with all other wallets + const tokenMetadata = await wallets[0].getContractMetadata(tokenContract.address); + await Promise.all( + wallets.slice(1).map(wallet => wallet.registerContract(tokenMetadata.instance!, TokenContract.artifact)), + ); + logger.info('Token contract registered with all wallets'); + + // Mint tokens publicly to each account (enough for TX_COUNT transfers). + // Send sequentially to avoid PXE concurrency issues, then wait in parallel. + logger.info(`Minting 1e18 tokens to each account...`); + const mintTxHashes = []; + for (const acc of accountAddresses) { + const { txHash } = await TokenContract.at(tokenContract.address, wallets[0]) + .methods.mint_to_public(acc, 10n ** 18n) + .send({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, wait: NO_WAIT }); + mintTxHashes.push(txHash); + } + await Promise.all(mintTxHashes.map(txHash => waitForTx(aztecNode, txHash, { timeout: 600 }))); + logger.info('Minting complete'); + }); + + it.each(TOKEN_TESTS)('measures block capacity with public token transfers', async (fnName, txCount) => { + // Each account transfers 1 token to a "sink" address. + // Note: For the clone path, all cloned txs share the same sender/recipient/amount. + // Public state conflicts may cause some cloned txs to fail during execution. + const recipient = accountAddresses[0]; + logger.info(`Pre-proving ${txCount} ${fnName} txs...`); + const txs = await proveOrCloneTxs(txCount, (wallet, addr) => { + const token = TokenContract.at(tokenContract.address, wallet); + return createProvableTx(wallet, addr, token.methods[fnName](addr, recipient, 1n, 0)); + }); + logger.info(`All ${txCount} ${fnName} txs pre-proven`); + const { blockTxCounts, enabledAt } = await floodAndMeasure(fnName, txs); + await recordBlockBuiltMetrics(fnName, blockTxCounts, enabledAt); + }); + }); +}); + +/** Clones a proven tx, randomizing nullifiers and updating fees so each clone is unique. */ +async function cloneTx(tx: Tx, aztecNode: AztecNode): Promise { + const clonedTx = Tx.clone(tx, false); + + // Fetch current minimum fees and apply 15x buffer to cover fee decay between blocks + const currentFees = await aztecNode.getCurrentMinFees(); + const paddedFees = currentFees.mul(E2E_DEFAULT_MIN_FEE_PADDING); + + // Update gas settings with current fees + (clonedTx.data.constants.txContext.gasSettings as any).maxFeesPerGas = paddedFees; + + // Randomize nullifiers to avoid conflicts + if (clonedTx.data.forRollup) { + for (let i = 0; i < clonedTx.data.forRollup.end.nullifiers.length; i++) { + if (clonedTx.data.forRollup.end.nullifiers[i].isZero()) { + continue; + } + clonedTx.data.forRollup.end.nullifiers[i] = Fr.random(); + } + } else if (clonedTx.data.forPublic) { + for (let i = 0; i < clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers.length; i++) { + if (clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers[i].isZero()) { + continue; + } + clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers[i] = Fr.random(); + } + } + + await clonedTx.recomputeHash(); + return clonedTx; +} diff --git a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts index 48986bd06c53..c98d5b0275ae 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps_prove.test.ts @@ -24,6 +24,7 @@ import type { ChildProcess } from 'child_process'; import { mkdir, writeFile } from 'fs/promises'; import { dirname } from 'path'; +import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js'; import { PrometheusClient } from '../quality_of_service/prometheus_client.js'; import type { WorkerWallet } from '../test-wallet/worker_wallet.js'; @@ -647,9 +648,9 @@ async function createTx( async function cloneTx(tx: Tx, aztecNode: AztecNode): Promise { const clonedTx = Tx.clone(tx, false); - // Fetch current minimum fees and apply 50% buffer for safety + // Fetch current minimum fees and apply 15x buffer to cover fee decay between blocks const currentFees = await aztecNode.getCurrentMinFees(); - const paddedFees = currentFees.mul(1.5); + const paddedFees = currentFees.mul(E2E_DEFAULT_MIN_FEE_PADDING); // Update gas settings with current fees (clonedTx.data.constants.txContext.gasSettings as any).maxFeesPerGas = paddedFees; diff --git a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts index 9407c10950fe..49657e6a706b 100644 --- a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts @@ -39,6 +39,8 @@ export interface AccountData { contract: AccountContract; } +const TEST_DEFAULT_MIN_FEE_PADDING = 10; + /** * Wallet implementation that stores accounts in memory and provides extra debugging * utilities @@ -50,6 +52,7 @@ export class TestWallet extends BaseWallet { private readonly nodeRef: AztecNodeProxy, ) { super(pxe, nodeRef); + this.minFeePadding = TEST_DEFAULT_MIN_FEE_PADDING; } static async create( @@ -143,7 +146,7 @@ export class TestWallet extends BaseWallet { } setMinFeePadding(value?: number) { - this.minFeePadding = value ?? 0.5; + this.minFeePadding = value ?? TEST_DEFAULT_MIN_FEE_PADDING; } protected getAccountFromAddress(address: AztecAddress): Promise { diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index 78878c6caf20..d42d90a652f2 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -8,6 +8,7 @@ import { AztecLMDBStoreV2, createStore } from '@aztec/kv-store/lmdb-v2'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; import type { AztecNode, ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -47,6 +48,7 @@ export async function createP2PClient( proofVerifier: ClientProtocolCircuitVerifier, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, + blockMinFeesProvider: BlockMinFeesProvider, packageVersion: string, dateProvider: DateProvider = new DateProvider(), telemetry: TelemetryClient = getTelemetryClient(), @@ -88,6 +90,7 @@ export async function createP2PClient( const currentBlockNumber = await archiver.getBlockNumber(); const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); const l1Constants = await archiver.getL1Constants(); + const gasFees = await blockMinFeesProvider.getCurrentMinFees(); return createTxValidatorForTransactionsEnteringPendingTxPool( worldStateSynchronizer, nextSlotTimestamp, @@ -97,8 +100,10 @@ export async function createP2PClient( maxBlockL2Gas: config.validateMaxL2BlockGas, maxBlockDAGas: config.validateMaxDABlockGas, }, + gasFees, ); }, + blockMinFeesProvider, }, telemetry, { diff --git a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts index e1f054b98a02..8d404210eca7 100644 --- a/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts +++ b/yarn-project/p2p/src/client/test/tx_proposal_collector/proposal_tx_collector_worker.ts @@ -7,6 +7,7 @@ import type { DataStoreConfig } from '@aztec/kv-store/config'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { GasFees } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier } from '@aztec/stdlib/interfaces/server'; import { PeerErrorSeverity } from '@aztec/stdlib/p2p'; import type { Tx, TxValidationResult } from '@aztec/stdlib/tx'; @@ -119,6 +120,7 @@ async function startClient(config: P2PConfig, clientIndex: number) { proofVerifier as ClientProtocolCircuitVerifier, worldState, epochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'proposal-tx-collector-bench-worker', new DateProvider(), telemetry as TelemetryClient, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts index 79abcdc12812..acbe9165c5b9 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/index.ts @@ -21,6 +21,7 @@ export { FeePayerBalancePreAddRule } from './fee_payer_balance_pre_add_rule.js'; export { LowPriorityPreAddRule } from './low_priority_pre_add_rule.js'; // Post-event eviction rules +export { InsufficientFeePerGasEvictionRule } from './insufficient_fee_per_gas_eviction_rule.js'; export { InvalidTxsAfterMiningRule } from './invalid_txs_after_mining_rule.js'; export { InvalidTxsAfterReorgRule } from './invalid_txs_after_reorg_rule.js'; export { FeePayerBalanceEvictionRule } from './fee_payer_balance_eviction_rule.js'; diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts new file mode 100644 index 000000000000..3f083c0c42b6 --- /dev/null +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.test.ts @@ -0,0 +1,185 @@ +import { BlockNumber } from '@aztec/foundation/branded-types'; +import { GasFees } from '@aztec/stdlib/gas'; +import { BlockHeader } from '@aztec/stdlib/tx'; + +import { jest } from '@jest/globals'; + +import { type TxMetaData, stubTxMetaData } from '../tx_metadata.js'; +import { InsufficientFeePerGasEvictionRule } from './insufficient_fee_per_gas_eviction_rule.js'; +import type { EvictionContext, PoolOperations } from './interfaces.js'; +import { EvictionEvent } from './interfaces.js'; + +describe('InsufficientFeePerGasEvictionRule', () => { + let pool: PoolOperations; + let rule: InsufficientFeePerGasEvictionRule; + let deleteTxsMock: jest.MockedFunction; + + const blockGasFees = new GasFees(10, 20); + + const createPoolOps = (pendingTxs: TxMetaData[]): PoolOperations => { + deleteTxsMock = jest.fn(() => Promise.resolve()); + return { + getPendingTxs: () => pendingTxs, + getPendingFeePayers: () => [...new Set(pendingTxs.map(t => t.feePayer))], + getFeePayerPendingTxs: (feePayer: string) => pendingTxs.filter(t => t.feePayer === feePayer), + getPendingTxCount: () => pendingTxs.length, + getLowestPriorityPending: () => [], + deleteTxs: deleteTxsMock as (txHashes: string[]) => Promise, + }; + }; + + beforeEach(() => { + pool = createPoolOps([]); + rule = new InsufficientFeePerGasEvictionRule({ getCurrentMinFees: () => Promise.resolve(blockGasFees) }); + }); + + describe('non-BLOCK_MINED events', () => { + it('returns empty result for TXS_ADDED event', async () => { + const context: EvictionContext = { + event: EvictionEvent.TXS_ADDED, + newTxHashes: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + }); + + it('returns empty result for CHAIN_PRUNED event', async () => { + const context: EvictionContext = { + event: EvictionEvent.CHAIN_PRUNED, + blockNumber: BlockNumber(1), + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + }); + }); + + describe('BLOCK_MINED events', () => { + let blockHeader: BlockHeader; + + beforeEach(() => { + blockHeader = BlockHeader.empty(); + blockHeader.globalVariables.blockNumber = BlockNumber(100); + blockHeader.globalVariables.timestamp = 1000n; + blockHeader.globalVariables.gasFees = new GasFees(10, 20); + }); + + it('evicts txs with insufficient DA fee per gas', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(9, 20) }); // DA too low + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(10, 20) }); // Exactly enough + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([tx1.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InsufficientFeePerGas'); + }); + + it('evicts txs with insufficient L2 fee per gas', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(10, 19) }); // L2 too low + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(10, 20) }); // Exactly enough + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([tx1.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx1.txHash], 'InsufficientFeePerGas'); + }); + + it('keeps txs with sufficient fees', async () => { + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(10, 20) }); + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(100, 200) }); + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result.success).toBe(true); + expect(result.txsEvicted).toEqual([]); + expect(deleteTxsMock).not.toHaveBeenCalled(); + }); + + it('handles empty pending list', async () => { + pool = createPoolOps([]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(result).toEqual({ + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }); + expect(deleteTxsMock).not.toHaveBeenCalled(); + }); + + it('uses blockMinFeesProvider to determine eviction threshold', async () => { + // blockMinFeesProvider returns lower projected fees (5, 10) than block header (10, 20) + const getCurrentMinFees = jest.fn(() => Promise.resolve(new GasFees(5, 10))); + rule = new InsufficientFeePerGasEvictionRule({ getCurrentMinFees }); + + const tx1 = stubTxMetaData('0x1111', { maxFeesPerGas: new GasFees(5, 10) }); // Sufficient for projected fees + const tx2 = stubTxMetaData('0x2222', { maxFeesPerGas: new GasFees(4, 10) }); // DA too low for projected fees + + pool = createPoolOps([tx1, tx2]); + + const context: EvictionContext = { + event: EvictionEvent.BLOCK_MINED, + block: blockHeader, + newNullifiers: [], + feePayers: [], + }; + + const result = await rule.evict(context, pool); + + expect(getCurrentMinFees).toHaveBeenCalled(); + expect(result.success).toBe(true); + // Only tx2 is evicted (DA fee 4 < projected 5), tx1 is kept despite block header fees being higher + expect(result.txsEvicted).toEqual([tx2.txHash]); + expect(deleteTxsMock).toHaveBeenCalledWith([tx2.txHash], 'InsufficientFeePerGas'); + }); + }); +}); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts new file mode 100644 index 000000000000..b096cf568a31 --- /dev/null +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/eviction/insufficient_fee_per_gas_eviction_rule.ts @@ -0,0 +1,65 @@ +import { createLogger } from '@aztec/foundation/log'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; + +import type { EvictionContext, EvictionResult, EvictionRule, PoolOperations } from './interfaces.js'; +import { EvictionEvent } from './interfaces.js'; + +/** + * Eviction rule that removes transactions whose maxFeesPerGas no longer meets + * the projected minimum gas fees after a new block is mined. + * Uses the BlockMinFeesProvider (forward-looking) to get the projected minimum fees. + * Only triggers on BLOCK_MINED events. + */ +export class InsufficientFeePerGasEvictionRule implements EvictionRule { + public readonly name = 'InsufficientFeePerGas'; + + private log = createLogger('p2p:tx_pool_v2:insufficient_fee_per_gas_eviction_rule'); + + constructor(private blockMinFeesProvider: BlockMinFeesProvider) {} + + async evict(context: EvictionContext, pool: PoolOperations): Promise { + if (context.event !== EvictionEvent.BLOCK_MINED) { + return { + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: [], + }; + } + + try { + const gasFees = await this.blockMinFeesProvider.getCurrentMinFees(); + const txsToEvict: string[] = []; + const pendingTxs = pool.getPendingTxs(); + + for (const meta of pendingTxs) { + const maxFeesPerGas = meta.data.constants.txContext.gasSettings.maxFeesPerGas; + if (maxFeesPerGas.feePerDaGas < gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < gasFees.feePerL2Gas) { + this.log.verbose(`Evicting tx ${meta.txHash} from pool due to insufficient fee per gas`, { + txMaxFeesPerGas: maxFeesPerGas.toInspect(), + blockGasFees: gasFees.toInspect(), + }); + txsToEvict.push(meta.txHash); + } + } + + if (txsToEvict.length > 0) { + this.log.info(`Evicted ${txsToEvict.length} txs with insufficient fee per gas after block mined`); + await pool.deleteTxs(txsToEvict, this.name); + } + + return { + reason: 'insufficient_fee_per_gas', + success: true, + txsEvicted: txsToEvict, + }; + } catch (err) { + this.log.error('Failed to evict transactions with insufficient fee per gas', { err }); + return { + reason: 'insufficient_fee_per_gas', + success: false, + txsEvicted: [], + error: new Error('Failed to evict txs with insufficient fee per gas', { cause: err }), + }; + } + } +} diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts index a54de6d1293b..88c064bcc4df 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts @@ -1,6 +1,7 @@ import type { SlotNumber } from '@aztec/foundation/branded-types'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import type { L2Block, L2BlockId, L2BlockSource } from '@aztec/stdlib/block'; +import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { BlockHeader, Tx, TxHash, TxValidator } from '@aztec/stdlib/tx'; @@ -69,6 +70,8 @@ export type TxPoolV2Dependencies = { worldStateSynchronizer: WorldStateSynchronizer; /** Factory that creates a validator for re-validating pool transactions using metadata */ createTxValidator: () => Promise>; + /** Provides projected minimum fees for the next block. Used by eviction rules instead of stale block header fees. */ + blockMinFeesProvider: BlockMinFeesProvider; }; /** diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts index 3874a7aab292..69fb36d352a1 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts @@ -2,7 +2,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { BlockHash, type L2BlockId } from '@aztec/stdlib/block'; -import { Gas } from '@aztec/stdlib/gas'; +import { Gas, GasFees } from '@aztec/stdlib/gas'; import { type Tx, TxHash } from '@aztec/stdlib/tx'; import { getFeePayerBalanceDelta } from '../../msg_validators/tx_validator/fee_payer_balance.js'; @@ -23,7 +23,7 @@ export type TxMetaValidationData = { }; }; txContext: { - gasSettings: { gasLimits: Gas }; + gasSettings: { gasLimits: Gas; maxFeesPerGas: GasFees }; }; }; }; @@ -124,7 +124,10 @@ export async function buildTxMetaData(tx: Tx): Promise { globalVariables: { blockNumber: anchorBlockNumber }, }, txContext: { - gasSettings: { gasLimits: tx.data.constants.txContext.gasSettings.gasLimits }, + gasSettings: { + gasLimits: tx.data.constants.txContext.gasSettings.gasLimits, + maxFeesPerGas: tx.data.constants.txContext.gasSettings.maxFeesPerGas, + }, }, }, }, @@ -277,7 +280,9 @@ export function checkNullifierConflict( } /** Creates a stub TxMetaValidationData for tests that don't exercise validators. */ -export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigint } = {}): TxMetaValidationData { +export function stubTxMetaValidationData( + overrides: { expirationTimestamp?: bigint; maxFeesPerGas?: GasFees } = {}, +): TxMetaValidationData { return { getNonEmptyNullifiers: () => [], expirationTimestamp: overrides.expirationTimestamp ?? 0n, @@ -287,7 +292,7 @@ export function stubTxMetaValidationData(overrides: { expirationTimestamp?: bigi globalVariables: { blockNumber: BlockNumber(0) }, }, txContext: { - gasSettings: { gasLimits: Gas.empty() }, + gasSettings: { gasLimits: Gas.empty(), maxFeesPerGas: overrides.maxFeesPerGas ?? GasFees.empty() }, }, }, }; @@ -304,6 +309,7 @@ export function stubTxMetaData( nullifiers?: string[]; expirationTimestamp?: bigint; anchorBlockHeaderHash?: string; + maxFeesPerGas?: GasFees; } = {}, ): TxMetaData { const txHashBigInt = Fr.fromHexString(txHash).toBigInt(); @@ -322,6 +328,6 @@ export function stubTxMetaData( expirationTimestamp, receivedAt: 0, estimatedSizeBytes: 0, - data: stubTxMetaValidationData({ expirationTimestamp }), + data: stubTxMetaValidationData({ expirationTimestamp, maxFeesPerGas: overrides.maxFeesPerGas }), }; } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts index c0d75aeec210..45f9685196d5 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts @@ -87,6 +87,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); }); @@ -327,6 +328,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { archivedTxLimit: 2 }, @@ -368,6 +370,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -424,6 +427,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -467,6 +471,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -639,6 +644,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 0 }, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts index 1d41cc370bf1..9a3380b3b768 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts @@ -27,7 +27,7 @@ import { BlockHeader, GlobalVariables, type Tx, TxEffect, TxHash, type TxValidat import { type MockProxy, mock } from 'jest-mock-extended'; -import { GasLimitsValidator } from '../../msg_validators/tx_validator/gas_validator.js'; +import { GasLimitsValidator, MaxFeePerGasValidator } from '../../msg_validators/tx_validator/gas_validator.js'; import type { TxMetaData } from './tx_metadata.js'; import { AztecKVTxPoolV2 } from './tx_pool_v2.js'; @@ -133,6 +133,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); @@ -542,6 +543,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(rejectingValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await rejectingPool.start(); }); @@ -657,6 +659,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(new GasLimitsValidator()), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await gasPool.start(); }); @@ -1292,6 +1295,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -2000,6 +2004,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -2181,6 +2186,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); }); @@ -4437,6 +4443,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4462,6 +4469,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4490,6 +4498,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 100 }, @@ -4516,6 +4525,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -4548,6 +4558,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4565,6 +4576,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4594,6 +4606,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4628,6 +4641,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { maxPendingTxCount: 0 }, // No pending txs allowed @@ -4656,6 +4670,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4682,6 +4697,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(selectiveValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4707,6 +4723,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -4732,6 +4749,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -4926,6 +4944,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, // telemetry { minTxPoolAgeMs: 2_000 }, @@ -5024,6 +5043,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }, undefined, { minTxPoolAgeMs: 2_000 }, @@ -5260,6 +5280,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await poolWithValidator.start(); @@ -5326,6 +5347,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -5360,6 +5382,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -5394,6 +5417,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(throwingValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -5419,6 +5443,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool2.start(); @@ -5432,4 +5457,246 @@ describe('TxPoolV2', () => { } }); }); + + describe('max fee per gas validation', () => { + let feePool: AztecKVTxPoolV2; + let feeStore: Awaited>; + let feeArchiveStore: Awaited>; + + // Block gas fees that the validator will compare against + const blockGasFees = new GasFees(10, 20); + + beforeEach(async () => { + feeStore = await openTmpStore('p2p'); + feeArchiveStore = await openTmpStore('archive'); + feePool = new AztecKVTxPoolV2(feeStore, feeArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(new MaxFeePerGasValidator(blockGasFees)), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, + }); + await feePool.start(); + }); + + afterEach(async () => { + await feePool.stop(); + await feeStore.delete(); + await feeArchiveStore.delete(); + }); + + const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { + const tx = await mockTx(seed, { numberOfNonRevertiblePublicCallRequests: 1 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas }); + return tx; + }; + + it('accepts tx with maxFeesPerGas exactly equal to block gas fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 20)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(1); + expect(result.rejected).toHaveLength(0); + }); + + it('accepts tx with maxFeesPerGas above block gas fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(100, 200)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(1); + expect(result.rejected).toHaveLength(0); + }); + + it('rejects tx with insufficient DA fee per gas', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(9, 20)); // DA too low + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('rejects tx with insufficient L2 fee per gas', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 19)); // L2 too low + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('rejects tx with both DA and L2 fee per gas insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 10)); + const result = await feePool.addPendingTxs([tx]); + expect(result.accepted).toHaveLength(0); + expect(toStrings(result.rejected)).toContain(hashOf(tx)); + }); + + it('handles batch with mixed sufficient and insufficient fees', async () => { + const txGood = await makeTxWithMaxFees(1, new GasFees(10, 20)); + const txBadDA = await makeTxWithMaxFees(2, new GasFees(9, 20)); + const txBadL2 = await makeTxWithMaxFees(3, new GasFees(10, 19)); + const txAlsoGood = await makeTxWithMaxFees(4, new GasFees(50, 50)); + + const result = await feePool.addPendingTxs([txGood, txBadDA, txBadL2, txAlsoGood]); + + expect(toStrings(result.accepted)).toContain(hashOf(txGood)); + expect(toStrings(result.accepted)).toContain(hashOf(txAlsoGood)); + expect(toStrings(result.rejected)).toContain(hashOf(txBadDA)); + expect(toStrings(result.rejected)).toContain(hashOf(txBadL2)); + expect(await feePool.getPendingTxCount()).toBe(2); + }); + }); + + describe('max fee per gas eviction after block mined', () => { + // The eviction rule uses getCurrentMinFees to determine the fee threshold. + // We use a mutable variable so each test can set the projected min fees. + let currentMinFees = GasFees.empty(); + + beforeEach(async () => { + // Re-create the pool with a getCurrentMinFees that returns the test-controlled value + await pool.stop(); + await store.delete(); + await archiveStore.delete(); + store = await openTmpStore('p2p'); + archiveStore = await openTmpStore('archive'); + currentMinFees = GasFees.empty(); + pool = new AztecKVTxPoolV2(store, archiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(currentMinFees) }, + }); + await pool.start(); + }); + + const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { + const tx = await mockTx(seed, { + numberOfNonRevertiblePublicCallRequests: 1, + maxPriorityFeesPerGas: new GasFees(1, 1), + }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ + maxFeesPerGas, + maxPriorityFeesPerGas: new GasFees(1, 1), + }); + return tx; + }; + + const headerWithGasFees = (gasFees: GasFees) => + BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ + blockNumber: BlockNumber(1), + slotNumber: SlotNumber(1), + timestamp: 0n, + gasFees, + }), + }); + + it('evicts pending txs when mined block has higher gas fees', async () => { + // Txs with maxFeesPerGas = (10, 10) + const tx1 = await makeTxWithMaxFees(1, new GasFees(10, 10)); + const tx2 = await makeTxWithMaxFees(2, new GasFees(10, 10)); + + await pool.addPendingTxs([tx1, tx2]); + expect(await pool.getPendingTxCount()).toBe(2); + + // Set projected min fees higher than txs' maxFeesPerGas + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Both txs should be evicted since their maxFeesPerGas (10, 10) < block fees (20, 20) + expect(await pool.getTxStatus(tx1.getTxHash())).toBe('deleted'); + expect(await pool.getTxStatus(tx2.getTxHash())).toBe('deleted'); + expect(await pool.getPendingTxCount()).toBe(0); + }); + + it('keeps pending txs when their maxFeesPerGas meets block gas fees', async () => { + // Txs with maxFeesPerGas = (50, 50) + const tx1 = await makeTxWithMaxFees(1, new GasFees(50, 50)); + const tx2 = await makeTxWithMaxFees(2, new GasFees(50, 50)); + + await pool.addPendingTxs([tx1, tx2]); + expect(await pool.getPendingTxCount()).toBe(2); + + // Set projected min fees lower than txs' maxFeesPerGas + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Both txs should remain pending since their maxFeesPerGas (50, 50) >= block fees (20, 20) + expect(await pool.getTxStatus(tx1.getTxHash())).toBe('pending'); + expect(await pool.getTxStatus(tx2.getTxHash())).toBe('pending'); + expect(await pool.getPendingTxCount()).toBe(2); + }); + + it('selectively evicts only txs with insufficient fees', async () => { + const txLowFee = await makeTxWithMaxFees(1, new GasFees(5, 5)); + const txHighFee = await makeTxWithMaxFees(2, new GasFees(50, 50)); + const txBorderline = await makeTxWithMaxFees(3, new GasFees(20, 20)); + + await pool.addPendingTxs([txLowFee, txHighFee, txBorderline]); + expect(await pool.getPendingTxCount()).toBe(3); + + // Set projected min fees to (20, 20) + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // txLowFee (5, 5) < (20, 20) -> evicted + expect(await pool.getTxStatus(txLowFee.getTxHash())).toBe('deleted'); + // txHighFee (50, 50) >= (20, 20) -> still pending + expect(await pool.getTxStatus(txHighFee.getTxHash())).toBe('pending'); + // txBorderline (20, 20) >= (20, 20) -> still pending (exactly equal is sufficient) + expect(await pool.getTxStatus(txBorderline.getTxHash())).toBe('pending'); + expect(await pool.getPendingTxCount()).toBe(2); + }); + + it('evicts when only DA fee is insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 50)); // DA too low, L2 fine + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('deleted'); + }); + + it('evicts when only L2 fee is insufficient', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(50, 5)); // L2 too low, DA fine + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('deleted'); + }); + + it('does not evict when block gas fees are zero', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(10, 10)); + + await pool.addPendingTxs([tx]); + expect(await pool.getPendingTxCount()).toBe(1); + + // Mine a block with zero gas fees (GasFees.empty) + await pool.handleMinedBlock(makeEmptyBlock(slot1Header)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('pending'); + }); + + it('does not evict protected txs even with insufficient fees', async () => { + const tx = await makeTxWithMaxFees(1, new GasFees(5, 5)); + + // Add as protected (not pending) + await pool.addProtectedTxs([tx], slot1Header); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + + // Set projected min fees higher than the tx's maxFeesPerGas + currentMinFees = new GasFees(20, 20); + const blockHeader = headerWithGasFees(new GasFees(20, 20)); + await pool.handleMinedBlock(makeEmptyBlock(blockHeader)); + + // Protected tx should not be evicted (eviction rules only check pending txs) + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + }); + }); }); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts index 8e196bcf3cfc..e880bd705633 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts @@ -141,6 +141,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool.start(); const cleanup = async () => { @@ -495,6 +496,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); await pool1.start(); @@ -512,6 +514,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + blockMinFeesProvider: { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, }); const startTime = performance.now(); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts index ca001974be1f..d638787930cc 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts @@ -17,6 +17,7 @@ import { EvictionManager, FeePayerBalanceEvictionRule, FeePayerBalancePreAddRule, + InsufficientFeePerGasEvictionRule, InvalidTxsAfterMiningRule, InvalidTxsAfterReorgRule, LowPriorityEvictionRule, @@ -113,6 +114,7 @@ export class TxPoolV2Impl { // Post-event eviction rules (run after events to check ALL pending txs) this.#evictionManager.registerRule(new InvalidTxsAfterMiningRule()); + this.#evictionManager.registerRule(new InsufficientFeePerGasEvictionRule(deps.blockMinFeesProvider)); this.#evictionManager.registerRule(new InvalidTxsAfterReorgRule(deps.worldStateSynchronizer)); this.#evictionManager.registerRule(new FeePayerBalanceEvictionRule(deps.worldStateSynchronizer)); // LowPriorityEvictionRule handles cases where txs become pending via prepareForSlot (unprotect) diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/README.md b/yarn-project/p2p/src/msg_validators/tx_validator/README.md index 91087c1559c5..1ae77d3bd190 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -75,7 +75,7 @@ This validator is invoked on **every** transaction potentially entering the pend - Startup hydration — revalidating persisted non-mined txs on node restart Runs: -- DoubleSpend, BlockHeader, GasLimits, Timestamp +- DoubleSpend, BlockHeader, GasLimits, MaxFeePerGas, Timestamp Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. @@ -89,8 +89,9 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | `MetadataTxValidator` | Chain ID, rollup version, protocol contracts hash, VK tree root | 4.18 us | | `TimestampTxValidator` | Transaction has not expired (expiration timestamp vs next slot) | 1.56 us | | `DoubleSpendTxValidator` | Nullifiers do not already exist in the nullifier tree | 106.08 us | -| `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees, and fee payer has sufficient FeeJuice balance | 1.02 ms | +| `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees (delegates to `MaxFeePerGasValidator`), and fee payer has sufficient FeeJuice balance | 1.02 ms | | `GasLimitsValidator` | Gas limits are >= fixed minimums and <= AVM max processable L2 gas. Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us | +| `MaxFeePerGasValidator` | Max fee per gas >= current block gas fees on both dimensions (DA and L2). Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us | | `PhasesTxValidator` | Public function calls in setup phase are on the allow list | 10.12–13.12 us | | `BlockHeaderTxValidator` | Transaction's anchor block hash exists in the archive tree | 98.88 us | | `TxProofValidator` | Client proof verifies correctly | ~250ms | @@ -107,9 +108,16 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | DoubleSpend | Stage 1 | Yes | — | Yes | Yes | | Gas (balance + limits) | Stage 1 | Optional* | — | Yes | — | | GasLimits (standalone) | — | — | — | — | Yes | +| MaxFeePerGas (standalone) | — | — | — | — | Yes | | Phases | Stage 1 | Yes | — | Yes | — | | BlockHeader | Stage 1 | Yes | — | Yes | Yes | | Proof | Stage 2 | Optional** | Yes | — | — | -\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` as its first step, so gas limits are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` standalone because it doesn't need the balance or fee-per-gas checks. +\* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` and `MaxFeePerGasValidator` as its first steps, so gas limits and fee-per-gas are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` and `MaxFeePerGasValidator` standalone because it doesn't need the balance check. \** Proof verification is skipped for simulations (no verifier provided). + +## Fee-Per-Gas Rejection Strategy + +The `MaxFeePerGasValidator` and `InsufficientFeePerGasEvictionRule` reject and evict transactions whose `maxFeesPerGas` falls below the current block's gas fees. This is a simple strategy: if a tx can't pay the current fees, it gets rejected on entry and evicted after each new block. + +**Caveat**: This may evict transactions that would become valid again if block fees drop. A more nuanced approach would be to define a threshold (e.g., 50%) and only reject/evict when the tx's max fee falls below that fraction of the current fees. The current approach is simpler and ensures the pool doesn't accumulate transactions with low max fees that are unlikely to be mined soon. diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts index 7cb2e794566c..890014117acd 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts @@ -25,7 +25,7 @@ import { createTxValidatorForReqResponseReceivedTxs, createTxValidatorForTransactionsEnteringPendingTxPool, } from './factory.js'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { PhasesTxValidator } from './phases_validator.js'; import { SizeTxValidator } from './size_validator.js'; @@ -304,11 +304,13 @@ describe('Validator factory functions', () => { 100n, BlockNumber(5), { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + new GasFees(1, 1), ); const aggregate = validator as AggregateTxValidator; expect(getValidatorNames(aggregate)).toEqual([ GasLimitsValidator.name, + MaxFeePerGasValidator.name, TimestampTxValidator.name, DoubleSpendTxValidator.name, BlockHeaderTxValidator.name, @@ -316,9 +318,13 @@ describe('Validator factory functions', () => { }); it('syncs world state before creating the validator', async () => { - await createTxValidatorForTransactionsEnteringPendingTxPool(synchronizer, 100n, BlockNumber(5), { - rollupManaLimit: Number.MAX_SAFE_INTEGER, - }); + await createTxValidatorForTransactionsEnteringPendingTxPool( + synchronizer, + 100n, + BlockNumber(5), + { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + new GasFees(1, 1), + ); expect(synchronizer.syncImmediate).toHaveBeenCalled(); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 9acd13d88c3d..371adf2ef89f 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -55,7 +55,7 @@ import { ArchiveCache } from './archive_cache.js'; import { type ArchiveSource, BlockHeaderTxValidator } from './block_header_validator.js'; import { DataTxValidator } from './data_validator.js'; import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_validator.js'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { NullifierCache } from './nullifier_cache.js'; import { PhasesTxValidator } from './phases_validator.js'; @@ -416,6 +416,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( timestamp: bigint, blockNumber: BlockNumber, gasLimitOpts: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }, + gasFees: GasFees, bindings?: LoggerBindings, ): Promise> { await worldStateSynchronizer.syncImmediate(); @@ -433,6 +434,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( }; return new AggregateTxValidator( new GasLimitsValidator({ ...gasLimitOpts, bindings }), + new MaxFeePerGasValidator(gasFees, bindings), new TimestampTxValidator({ timestamp, blockNumber }, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), new BlockHeaderTxValidator(archiveSource, bindings), diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts index 719be28a8454..1b0fe02e5935 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts @@ -27,7 +27,7 @@ import { import assert from 'assert'; import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; -import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; +import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { patchNonRevertibleFn, patchRevertibleFn } from './test_utils.js'; describe('GasTxValidator', () => { @@ -75,10 +75,6 @@ describe('GasTxValidator', () => { await expect(validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [reason] }); }; - const expectSkipped = async (tx: Tx, reason: string) => { - await expect(validateTx(tx)).resolves.toEqual({ result: 'skipped', reason: [reason] }); - }; - it('allows fee paying txs if fee payer has enough balance', async () => { mockBalance(feeLimit); await expectValid(tx); @@ -351,13 +347,45 @@ describe('GasTxValidator', () => { }); }); - it('skips txs with not enough fee per da gas', async () => { + it('rejects txs with not enough fee per da gas', async () => { gasFees.feePerDaGas = gasFees.feePerDaGas + 1n; - await expectSkipped(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); }); - it('skips txs with not enough fee per l2 gas', async () => { + it('rejects txs with not enough fee per l2 gas', async () => { gasFees.feePerL2Gas = gasFees.feePerL2Gas + 1n; - await expectSkipped(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + await expectInvalid(tx, TX_ERROR_INSUFFICIENT_FEE_PER_GAS); + }); +}); + +describe('MaxFeePerGasValidator', () => { + it('accepts tx with sufficient max fees per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(10, 20) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); + }); + + it('rejects tx with insufficient DA fee per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(9, 20) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS], + }); + }); + + it('rejects tx with insufficient L2 fee per gas', async () => { + const gasFees = new GasFees(10, 20); + const validator = new MaxFeePerGasValidator(gasFees); + const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); + tx.data.constants.txContext.gasSettings = GasSettings.default({ maxFeesPerGas: new GasFees(10, 19) }); + await expect(validator.validateTx(tx)).resolves.toEqual({ + result: 'invalid', + reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS], + }); }); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts index 19b94402e324..e791024232d0 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts @@ -36,6 +36,18 @@ export interface HasGasLimitData { }; } +/** Structural interface for types that carry max fee per gas data, used by {@link MaxFeePerGasValidator}. */ +export interface HasMaxFeePerGasData { + txHash: { toString(): string }; + data: { + constants: { + txContext: { + gasSettings: { maxFeesPerGas: GasFees }; + }; + }; + }; +} + /** * Validates that a transaction's gas limits are within acceptable bounds. * @@ -113,15 +125,55 @@ export class GasLimitsValidator implements TxValidato } } +/** + * Validates that a transaction's max fee per gas meets the current block's gas fees. + * + * Rejects transactions whose maxFeesPerGas is below the current block's gas fees + * on either dimension (DA or L2). This is a cheap, stateless check. + * + * Generic over T so it can validate both full {@link Tx} objects and {@link TxMetaData} + * (used during pending pool migration). + * + * Used by: pending pool migration (via factory), and indirectly by {@link GasTxValidator}. + */ +export class MaxFeePerGasValidator implements TxValidator { + #log: Logger; + #gasFees: GasFees; + + constructor(gasFees: GasFees, bindings?: LoggerBindings) { + this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); + this.#gasFees = gasFees; + } + + validateTx(tx: T): Promise { + return Promise.resolve(this.validateMaxFeePerGas(tx)); + } + + /** Checks maxFeesPerGas >= current block gas fees on both dimensions. */ + validateMaxFeePerGas(tx: T): TxValidationResult { + const maxFeesPerGas = tx.data.constants.txContext.gasSettings.maxFeesPerGas; + const notEnoughMaxFees = + maxFeesPerGas.feePerDaGas < this.#gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < this.#gasFees.feePerL2Gas; + + if (notEnoughMaxFees) { + this.#log.verbose(`Rejecting transaction ${tx.txHash.toString()} due to insufficient fee per gas`, { + txMaxFeesPerGas: maxFeesPerGas.toInspect(), + currentGasFees: this.#gasFees.toInspect(), + }); + return { result: 'invalid', reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS] }; + } + return { result: 'valid' }; + } +} + /** * Validates that a transaction can pay its gas fees. * * Runs three checks in order: * 1. **Gas limits** (delegates to {@link GasLimitsValidator}) — rejects if limits are * out of bounds. - * 2. **Max fee per gas** — skips (not rejects) the tx if its maxFeesPerGas is below - * the current block's gas fees. We skip rather than reject because the tx may - * become eligible in a later block with lower fees. + * 2. **Max fee per gas** — rejects the tx if its maxFeesPerGas is below + * the current block's gas fees. * 3. **Fee payer balance** — reads the fee payer's FeeJuice balance from public state, * adds any pending claim from a setup-phase `_increase_public_balance` call, and * rejects if the total is less than the tx's fee limit (gasLimits * maxFeePerGas). @@ -155,37 +207,15 @@ export class GasTxValidator implements TxValidator { bindings: this.bindings, }).validateGasLimit(tx); if (gasLimitValidation.result === 'invalid') { - return Promise.resolve(gasLimitValidation); + return gasLimitValidation; } - if (this.#shouldSkip(tx)) { - return Promise.resolve({ result: 'skipped', reason: [TX_ERROR_INSUFFICIENT_FEE_PER_GAS] }); + const maxFeeValidation = new MaxFeePerGasValidator(this.#gasFees, this.bindings).validateMaxFeePerGas(tx); + if (maxFeeValidation.result === 'invalid') { + return maxFeeValidation; } return await this.validateTxFee(tx); } - /** - * Check whether the tx's max fees are valid for the current block, and skip if not. - * We skip instead of invalidating since the tx may become eligible later. - * Note that circuits check max fees even if fee payer is unset, so we - * keep this validation even if the tx does not pay fees. - */ - #shouldSkip(tx: Tx): boolean { - const gasSettings = tx.data.constants.txContext.gasSettings; - - // Skip the tx if its max fees are not enough for the current block's gas fees. - const maxFeesPerGas = gasSettings.maxFeesPerGas; - const notEnoughMaxFees = - maxFeesPerGas.feePerDaGas < this.#gasFees.feePerDaGas || maxFeesPerGas.feePerL2Gas < this.#gasFees.feePerL2Gas; - - if (notEnoughMaxFees) { - this.#log.verbose(`Skipping transaction ${tx.getTxHash().toString()} due to insufficient fee per gas`, { - txMaxFeesPerGas: maxFeesPerGas.toInspect(), - currentGasFees: this.#gasFees.toInspect(), - }); - } - return notEnoughMaxFees; - } - /** * Checks the fee payer has enough FeeJuice balance to cover the tx's fee limit. * Accounts for any pending claim from a setup-phase `_increase_public_balance` call. diff --git a/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts b/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts index 1bb554f79b06..101d40173293 100644 --- a/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts +++ b/yarn-project/p2p/src/test-helpers/make-test-p2p-clients.ts @@ -6,6 +6,7 @@ import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { DataStoreConfig } from '@aztec/kv-store/config'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; +import { GasFees } from '@aztec/stdlib/gas'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { createP2PClient } from '../client/index.js'; @@ -102,6 +103,7 @@ export async function makeTestP2PClient( proofVerifier, mockWorldState, mockEpochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'test-p2p-client', undefined, undefined, diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index 845ea7190f26..8f370a306afc 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -18,6 +18,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { GasFees } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { type BlockProposal, P2PMessage } from '@aztec/stdlib/p2p'; import { ChonkProof } from '@aztec/stdlib/proofs'; @@ -369,6 +370,7 @@ process.on('message', async msg => { proofVerifier as ClientProtocolCircuitVerifier, worldState, epochCache, + { getCurrentMinFees: () => Promise.resolve(GasFees.empty()) }, 'test-p2p-bench-worker', undefined, telemetry as TelemetryClient, diff --git a/yarn-project/stdlib/src/gas/gas_fees.ts b/yarn-project/stdlib/src/gas/gas_fees.ts index 7387b2df0496..6d3ee5112168 100644 --- a/yarn-project/stdlib/src/gas/gas_fees.ts +++ b/yarn-project/stdlib/src/gas/gas_fees.ts @@ -126,3 +126,8 @@ export class GasFees { return `GasFees { feePerDaGas=${this.feePerDaGas} feePerL2Gas=${this.feePerL2Gas} }`; } } + +/** Provides projected minimum gas fees for the next block. */ +export interface BlockMinFeesProvider { + getCurrentMinFees(): Promise; +} diff --git a/yarn-project/stdlib/src/tx/global_variable_builder.ts b/yarn-project/stdlib/src/tx/global_variable_builder.ts index 7cc64ab7bf18..b2460516a182 100644 --- a/yarn-project/stdlib/src/tx/global_variable_builder.ts +++ b/yarn-project/stdlib/src/tx/global_variable_builder.ts @@ -2,16 +2,14 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; import type { SlotNumber } from '@aztec/foundation/schemas'; import type { AztecAddress } from '../aztec-address/index.js'; -import type { GasFees } from '../gas/gas_fees.js'; +import type { BlockMinFeesProvider } from '../gas/gas_fees.js'; import type { UInt32 } from '../types/index.js'; import type { CheckpointGlobalVariables, GlobalVariables } from './global_variables.js'; /** * Interface for building global variables for Aztec blocks. */ -export interface GlobalVariableBuilder { - getCurrentMinFees(): Promise; - +export interface GlobalVariableBuilder extends BlockMinFeesProvider { /** * Builds global variables for a given block. * @param blockNumber - The block number to build global variables for. From 96d4ab8583402303c8a12ed2153deddd531a7d61 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Thu, 12 Mar 2026 15:10:15 +0000 Subject: [PATCH 5/7] fix: resolve cherry-pick conflicts Remove yarn-project/end-to-end/src/spartan/block_capacity.test.ts which was deleted in v4-next but modified by the cherry-picked commit. The file does not exist on the target branch so the modification is dropped. --- .../src/spartan/block_capacity.test.ts | 477 ------------------ 1 file changed, 477 deletions(-) delete mode 100644 yarn-project/end-to-end/src/spartan/block_capacity.test.ts diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts deleted file mode 100644 index 10621320ed1a..000000000000 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { type ContractFunctionInteraction, NO_WAIT, toSendOptions } from '@aztec/aztec.js/contracts'; -import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; -import { type AztecNode, createAztecNodeClient, waitForTx } from '@aztec/aztec.js/node'; -import { AccountManager } from '@aztec/aztec.js/wallet'; -import { asyncPool } from '@aztec/foundation/async-pool'; -import { BlockNumber } from '@aztec/foundation/branded-types'; -import { times } from '@aztec/foundation/collection'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import { createLogger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; -import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; -import { GasFees } from '@aztec/stdlib/gas'; -import { deriveSigningKey } from '@aztec/stdlib/keys'; -import { Tx } from '@aztec/stdlib/tx'; - -import { jest } from '@jest/globals'; -import { mkdir, writeFile } from 'fs/promises'; -import { dirname } from 'path'; - -import { E2E_DEFAULT_MIN_FEE_PADDING } from '../fixtures/fixtures.js'; -import { getSponsoredFPCAddress, registerSponsoredFPC } from '../fixtures/utils.js'; -import type { WorkerWallet } from '../test-wallet/worker_wallet.js'; -import { type WorkerWalletWrapper, createWorkerWalletClient } from './setup_test_wallets.js'; -import { - fetchBlockBuiltLogs, - getExternalIP, - getSequencersConfig, - setupEnvironment, - updateSequencersConfig, -} from './utils.js'; - -const config = setupEnvironment(process.env); -const txRealProofs = config.REAL_VERIFIER || config.DEBUG_FORCE_TX_PROOF_VERIFICATION; - -const BENCH_TESTS = [ - ['noop', 100], - ['noop_pub', 100], - ['emit_nullifiers', 100], - ['emit_note_hashes', 100], - ['emit_l2_to_l1_msgs', 100], - ['emit_private_logs', 88], // we run out of blob space - ['emit_contract_class_log', 8], -] as const; - -const TOKEN_TESTS = [ - // intentional comment - for file fomatting - ['transfer_in_public', 100], -] as const; - -const maxTxs = Math.max(...[...BENCH_TESTS, ...TOKEN_TESTS].map(t => t[1])); -const NUM_WALLETS = txRealProofs ? Math.min(10, maxTxs) : 1; - -describe('block capacity benchmark', () => { - jest.setTimeout(60 * 60 * 1000); // 60 minutes - - const logger = createLogger('e2e:spartan-test:block-capacity'); - - let testWallets: WorkerWalletWrapper[]; - let wallets: WorkerWallet[]; - let accountAddresses: AztecAddress[]; - let aztecNode: AztecNode; - let originalSequencerConfig: Awaited> | undefined; - const benchmarkData: Array<{ name: string; unit: string; value: number }> = []; - - beforeAll(async () => { - logger.info('Setting up block capacity benchmark', { - numWallets: NUM_WALLETS, - txRealProofs, - namespace: config.NAMESPACE, - }); - - await updateSequencersConfig(config, { minTxsPerBlock: 0 }); - - const rpcIP = await getExternalIP(config.NAMESPACE, 'rpc-aztec-node'); - const rpcUrl = `http://${rpcIP}:8080`; - aztecNode = createAztecNodeClient(rpcUrl); - - // Wait for node to be ready - await retryUntil(async () => await aztecNode.isReady(), 'node ready', 120, 1); - logger.info('Node is ready'); - - // Save original sequencer config for restoration - originalSequencerConfig = await getSequencersConfig(config); - logger.info('Saved original sequencer config', { - minTxsPerBlock: originalSequencerConfig[0]?.minTxsPerBlock, - }); - - // Create WorkerWallets in parallel - logger.info(`Creating ${NUM_WALLETS} worker wallet(s)...`); - testWallets = await Promise.all( - Array.from({ length: NUM_WALLETS }, (_, i) => { - logger.info(`Creating wallet ${i + 1}/${NUM_WALLETS}`); - return createWorkerWalletClient(rpcUrl, txRealProofs, logger); - }), - ); - wallets = testWallets.map(tw => tw.wallet); - - // Register FPC and create/deploy accounts in parallel - const fpcAddress = await getSponsoredFPCAddress(); - const sponsor = new SponsoredFeePaymentMethod(fpcAddress); - accountAddresses = await Promise.all( - wallets.map(async wallet => { - const secret = Fr.random(); - const salt = Fr.random(); - const address = await wallet.registerAccount(secret, salt); - await registerSponsoredFPC(wallet); - const manager = await AccountManager.create( - wallet, - secret, - new SchnorrAccountContract(deriveSigningKey(secret)), - salt, - ); - const deployMethod = await manager.getDeployMethod(); - await deployMethod.send({ - from: AztecAddress.ZERO, - fee: { paymentMethod: sponsor }, - wait: { timeout: 2400 }, - }); - logger.info(`Account deployed at ${address}`); - return address; - }), - ); - }); - - afterAll(async () => { - // Write benchmark output if configured - if (process.env.BENCH_OUTPUT && benchmarkData.length > 0) { - const scenario = process.env.BENCH_SCENARIO?.trim(); - const finalData = scenario - ? benchmarkData.map(e => ({ ...e, name: `scenario/${scenario}/${e.name}` })) - : benchmarkData; - await mkdir(dirname(process.env.BENCH_OUTPUT), { recursive: true }); - await writeFile(process.env.BENCH_OUTPUT, JSON.stringify(finalData)); - logger.info('Wrote benchmark output', { path: process.env.BENCH_OUTPUT, entries: finalData.length }); - } - - // Restore original sequencer config - if (originalSequencerConfig?.[0]) { - logger.info('Restoring original sequencer config'); - await updateSequencersConfig(config, originalSequencerConfig[0]); - } - - if (testWallets) { - for (const tw of testWallets) { - await tw.cleanup(); - } - } - - logger.info('Cleanup complete'); - }); - - /** Creates and proves a single tx from a contract interaction. */ - async function createProvableTx( - wallet: WorkerWallet, - accountAddress: AztecAddress, - interaction: ContractFunctionInteraction, - ): Promise { - const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); - const options = { - from: accountAddress, - fee: { paymentMethod: sponsor, gasSettings: { maxPriorityFeesPerGas: GasFees.empty() } }, - }; - const execPayload = await interaction.request(options); - return wallet.proveTx(execPayload, toSendOptions(options)); - } - - /** Pre-proves TX_COUNT txs, either in parallel batches or by cloning a prototype. */ - async function proveOrCloneTxs( - txCount: number, - createPrototypeFn: (wallet: WorkerWallet, accountAddress: AztecAddress) => Promise, - ): Promise { - const txs: Tx[] = []; - if (txRealProofs) { - for (let i = 0; i < txCount; i += NUM_WALLETS) { - const batchSize = Math.min(NUM_WALLETS, txCount - txs.length); - const batchTxs = await Promise.all(times(batchSize, j => createPrototypeFn(wallets[j], accountAddresses[j]))); - txs.push(...batchTxs); - logger.info(`Proved ${txs.length}/${txCount} txs`); - } - } else { - const prototypeTx = await createPrototypeFn(wallets[0], accountAddresses[0]); - logger.info('Prototype tx proved, cloning...'); - for (let i = 0; i < txCount; i++) { - txs.push(await cloneTx(prototypeTx, aztecNode)); - if ((i + 1) % 10 === 0 || i === txCount - 1) { - logger.info(`Cloned ${i + 1}/${txCount} txs`); - } - } - } - return txs; - } - - /** Floods the mempool with pre-proven txs and measures block capacity. */ - async function floodAndMeasure( - label: string, - provenTxs: Tx[], - ): Promise<{ blockTxCounts: { blockNumber: number; txCount: number }[]; enabledAt: string }> { - const epochDurationSec = 2 * config.AZTEC_EPOCH_DURATION * config.AZTEC_SLOT_DURATION; // wait for up to two epochs (these are shorter epochs than standard) - const txCount = provenTxs.length; - - // 0. wait for the mempool to clear - await retryUntil( - async () => { - const pendingTxs = await aztecNode.getPendingTxCount(); - - if (pendingTxs > 0) { - logger.info(`Waiting for mempool to clear before sending test txs: ${pendingTxs} pending txs left.`); - return false; - } else { - return true; - } - }, - 'clear pending txs', - epochDurationSec, - 1, - ); - - // 1. Disable block building by setting minTxsPerBlock extremely high - logger.info(`[${label}] Disabling block building`); - await updateSequencersConfig(config, { minTxsPerBlock: 999_999_999 }); - await retryUntil( - async () => { - const configs = await getSequencersConfig(config); - return configs.every(c => c.minTxsPerBlock === 999_999_999); - }, - 'disable block building', - 60, - 1, - ); - logger.info(`[${label}] Block building disabled`); - - const blockBeforeFlood = await aztecNode.getBlockNumber(); - logger.info(`[${label}] Block number before flood`, { blockBeforeFlood }); - - // 2. Send all pre-proven txs to mempool - logger.info(`[${label}] Sending ${provenTxs.length} pre-proven txs to mempool`); - const sendStartMs = Date.now(); - - let sentCount = 0; - const txSize = provenTxs[0].toBuffer().length; - logger.info(`Tx size: ${(txSize / 1024 / 1024).toFixed(2)}MB (${txSize} bytes)`); - // dynamically adjust how many txs we can send to stay below 1MB - await asyncPool(Math.max(1, Math.floor((0.5 * 1024 * 1024) / txSize)), provenTxs, async tx => { - await aztecNode.sendTx(tx); - sentCount++; - if (sentCount % 10 === 0 || sentCount === provenTxs.length) { - logger.info(`[${label}] Sent ${sentCount}/${provenTxs.length} txs`); - } - }); - - const sendDurationMs = Date.now() - sendStartMs; - logger.info(`[${label}] All ${provenTxs.length} txs sent to mempool`, { sendDurationMs }); - - // 3. Re-enable block building - const enabledAt = new Date().toISOString(); - logger.info(`[${label}] Re-enabling block building`); - await updateSequencersConfig(config, { minTxsPerBlock: 1, enforceTimeTable: true }); - await retryUntil( - async () => { - const configs = await getSequencersConfig(config); - return configs.every(c => c.minTxsPerBlock === 1); - }, - 'enable block building', - 60, - 1, - ); - logger.info(`[${label}] Block building re-enabled`); - - // 4. Wait for blocks and observe inclusion - let totalTxsMined = 0; - const blockTxCounts: { blockNumber: number; txCount: number }[] = []; - - await retryUntil( - async () => { - const currentBlock = await aztecNode.getBlockNumber(); - for (let bn = blockBeforeFlood + 1; bn <= currentBlock; bn++) { - if (blockTxCounts.some(b => b.blockNumber === bn)) { - continue; - } - const block = await aztecNode.getBlock(BlockNumber(bn)); - if (block) { - const txCount = block.body.txEffects.length; - blockTxCounts.push({ blockNumber: bn, txCount }); - totalTxsMined += txCount; - logger.info(`[${label}] Block ${bn}: ${txCount} txs (total mined: ${totalTxsMined}/${txCount})`); - } - } - return totalTxsMined >= txCount; - }, - 'all txs mined', - epochDurationSec, - 1, - ); - - // Log summary - logger.info(`=== Block Capacity Benchmark Results (${label}) ===`); - logger.info(`Total txs sent: ${txCount}`); - logger.info(`Total txs mined: ${totalTxsMined}`); - logger.info(`Blocks produced: ${blockTxCounts.length}`); - for (const { blockNumber, txCount } of blockTxCounts) { - logger.info(` Block ${blockNumber}: ${txCount} txs`); - } - - if (blockTxCounts.length > 0) { - const maxTxsInBlock = Math.max(...blockTxCounts.map(b => b.txCount)); - const avgTxsPerBlock = totalTxsMined / blockTxCounts.length; - logger.info(`Max txs in a single block: ${maxTxsInBlock}`); - logger.info(`Avg txs per block: ${avgTxsPerBlock.toFixed(1)}`); - } - - expect(totalTxsMined).toBeGreaterThanOrEqual(txCount); - - return { blockTxCounts, enabledAt }; - } - - /** Fetches block-built stats from sequencer logs and records benchmark metrics for each block. */ - async function recordBlockBuiltMetrics( - label: string, - blockTxCounts: { blockNumber: number; txCount: number }[], - enabledAt: string, - ): Promise { - const blockNumbers = new Set(blockTxCounts.map(b => b.blockNumber)); - const entries = await fetchBlockBuiltLogs(config.NAMESPACE, enabledAt, blockNumbers, logger); - - if (entries.length === 0) { - logger.warn(`[${label}] No block-built log entries found, skipping benchmark metrics`); - return; - } - - // Record metrics for each block (entries are sorted by blockNumber ascending) - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - const prefix = `block_capacity/${label}/block_${i}`; - benchmarkData.push( - { name: `${prefix}/duration`, unit: 'ms', value: entry.duration }, - { name: `${prefix}/tx_count`, unit: 'count', value: entry.txCount }, - { name: `${prefix}/mana_per_sec`, unit: 'mana/s', value: entry.manaPerSec }, - { name: `${prefix}/public_process_duration`, unit: 'ms', value: entry.publicProcessDuration }, - { name: `${prefix}/private_log_count`, unit: 'count', value: entry.privateLogCount }, - { name: `${prefix}/public_log_count`, unit: 'count', value: entry.publicLogCount }, - { name: `${prefix}/contract_class_log_count`, unit: 'count', value: entry.contractClassLogCount }, - { name: `${prefix}/contract_class_log_size`, unit: 'fields', value: entry.contractClassLogSize }, - ); - logger.info(`[${label}] Recorded benchmark metrics from block ${entry.blockNumber} (index ${i})`, entry); - } - - benchmarkData.push({ - name: `block_capacity/${label}/blocks_produced`, - unit: 'count', - value: entries.length, - }); - } - - describe('Benchmark contract', () => { - let benchmarkContract: BenchmarkingContract; - - beforeAll(async () => { - const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); - // Deploy BenchmarkingContract using the first wallet - logger.info('Deploying benchmark contract...'); - ({ contract: benchmarkContract } = await BenchmarkingContract.deploy(wallets[0]).send({ - from: accountAddresses[0], - fee: { paymentMethod: sponsor }, - })); - logger.info('BenchmarkingContract deployed', { address: benchmarkContract.address.toString() }); - - // Register benchmark contract with all other wallets - const benchMetadata = await wallets[0].getContractMetadata(benchmarkContract.address); - await Promise.all( - wallets.slice(1).map(wallet => wallet.registerContract(benchMetadata.instance!, BenchmarkingContract.artifact)), - ); - logger.info('Benchmark contract registered with all wallets'); - }); - it.each(BENCH_TESTS)('measures block capacity with %s', async (fnName, txCount) => { - logger.info(`Pre-proving ${txCount} ${fnName} txs...`); - const txs = await proveOrCloneTxs(txCount, (wallet, addr) => { - const contract = BenchmarkingContract.at(benchmarkContract.address, wallet); - return createProvableTx(wallet, addr, contract.methods[fnName]()); - }); - logger.info(`All ${txCount} ${fnName} txs pre-proven`); - const { blockTxCounts, enabledAt } = await floodAndMeasure(fnName, txs); - await recordBlockBuiltMetrics(fnName, blockTxCounts, enabledAt); - }); - }); - - describe('Token contract', () => { - let tokenContract: TokenContract; - - beforeAll(async () => { - const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); - // Deploy TokenContract using the first wallet - logger.info('Deploying token contract...'); - ({ contract: tokenContract } = await TokenContract.deploy( - wallets[0], - accountAddresses[0], - 'USDC', - 'USD', - 18n, - ).send({ - from: accountAddresses[0], - fee: { paymentMethod: sponsor }, - wait: { timeout: 600 }, - })); - logger.info('TokenContract deployed', { address: tokenContract.address.toString() }); - - // Register token contract with all other wallets - const tokenMetadata = await wallets[0].getContractMetadata(tokenContract.address); - await Promise.all( - wallets.slice(1).map(wallet => wallet.registerContract(tokenMetadata.instance!, TokenContract.artifact)), - ); - logger.info('Token contract registered with all wallets'); - - // Mint tokens publicly to each account (enough for TX_COUNT transfers). - // Send sequentially to avoid PXE concurrency issues, then wait in parallel. - logger.info(`Minting 1e18 tokens to each account...`); - const mintTxHashes = []; - for (const acc of accountAddresses) { - const { txHash } = await TokenContract.at(tokenContract.address, wallets[0]) - .methods.mint_to_public(acc, 10n ** 18n) - .send({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, wait: NO_WAIT }); - mintTxHashes.push(txHash); - } - await Promise.all(mintTxHashes.map(txHash => waitForTx(aztecNode, txHash, { timeout: 600 }))); - logger.info('Minting complete'); - }); - - it.each(TOKEN_TESTS)('measures block capacity with public token transfers', async (fnName, txCount) => { - // Each account transfers 1 token to a "sink" address. - // Note: For the clone path, all cloned txs share the same sender/recipient/amount. - // Public state conflicts may cause some cloned txs to fail during execution. - const recipient = accountAddresses[0]; - logger.info(`Pre-proving ${txCount} ${fnName} txs...`); - const txs = await proveOrCloneTxs(txCount, (wallet, addr) => { - const token = TokenContract.at(tokenContract.address, wallet); - return createProvableTx(wallet, addr, token.methods[fnName](addr, recipient, 1n, 0)); - }); - logger.info(`All ${txCount} ${fnName} txs pre-proven`); - const { blockTxCounts, enabledAt } = await floodAndMeasure(fnName, txs); - await recordBlockBuiltMetrics(fnName, blockTxCounts, enabledAt); - }); - }); -}); - -/** Clones a proven tx, randomizing nullifiers and updating fees so each clone is unique. */ -async function cloneTx(tx: Tx, aztecNode: AztecNode): Promise { - const clonedTx = Tx.clone(tx, false); - - // Fetch current minimum fees and apply 15x buffer to cover fee decay between blocks - const currentFees = await aztecNode.getCurrentMinFees(); - const paddedFees = currentFees.mul(E2E_DEFAULT_MIN_FEE_PADDING); - - // Update gas settings with current fees - (clonedTx.data.constants.txContext.gasSettings as any).maxFeesPerGas = paddedFees; - - // Randomize nullifiers to avoid conflicts - if (clonedTx.data.forRollup) { - for (let i = 0; i < clonedTx.data.forRollup.end.nullifiers.length; i++) { - if (clonedTx.data.forRollup.end.nullifiers[i].isZero()) { - continue; - } - clonedTx.data.forRollup.end.nullifiers[i] = Fr.random(); - } - } else if (clonedTx.data.forPublic) { - for (let i = 0; i < clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers.length; i++) { - if (clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers[i].isZero()) { - continue; - } - clonedTx.data.forPublic.nonRevertibleAccumulatedData.nullifiers[i] = Fr.random(); - } - } - - await clonedTx.recomputeHash(); - return clonedTx; -} From f62def9610348dd4f72c108d4f37122ff86b0878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 13 Mar 2026 04:38:51 +0700 Subject: [PATCH 6/7] cherry-pick: fix: complete legacy oracle mappings for all pinned contracts (#21404) Cherry-pick of 1019f2a65a with conflicts preserved for review. --- .../oracle/legacy_oracle_mappings.ts | 77 +++++++++++++++++++ .../oracle/oracle.ts | 33 ++++++++ .../oracle/utility_execution_oracle.ts | 23 ++++++ 3 files changed, 133 insertions(+) create mode 100644 yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts new file mode 100644 index 000000000000..ee95befc339a --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -0,0 +1,77 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import type { ACIRCallback, ACVMField } from '@aztec/simulator/client'; + +import type { Oracle } from './oracle.js'; + +/** + * Builds legacy oracle name callbacks for pinned protocol contracts whose artifacts are committed and cannot be + * changed. + * TODO(F-416): Remove these aliases on v5 when protocol contracts are redeployed. + */ +export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { + return { + // Simple prefix renames (privateXxx/utilityXxx → aztec_prv_/aztec_utl_) + utilityLog: (...args: ACVMField[][]) => oracle.aztec_utl_log(args[0], args[1], args[2], args[3]), + utilityAssertCompatibleOracleVersion: (...args: ACVMField[][]) => + oracle.aztec_utl_assertCompatibleOracleVersion(args[0]), + utilityLoadCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_loadCapsule(args[0], args[1], args[2]), + privateStoreInExecutionCache: (...args: ACVMField[][]) => oracle.aztec_prv_storeInExecutionCache(args[0], args[1]), + privateLoadFromExecutionCache: (...args: ACVMField[][]) => oracle.aztec_prv_loadFromExecutionCache(args[0]), + privateCallPrivateFunction: (...args: ACVMField[][]) => + oracle.aztec_prv_callPrivateFunction(args[0], args[1], args[2], args[3], args[4]), + privateIsNullifierPending: (...args: ACVMField[][]) => oracle.aztec_prv_isNullifierPending(args[0], args[1]), + privateNotifyCreatedNullifier: (...args: ACVMField[][]) => oracle.aztec_prv_notifyCreatedNullifier(args[0]), + privateNotifyCreatedContractClassLog: (...args: ACVMField[][]) => + oracle.aztec_prv_notifyCreatedContractClassLog(args[0], args[1], args[2], args[3]), + privateGetNextAppTagAsSender: (...args: ACVMField[][]) => oracle.aztec_prv_getNextAppTagAsSender(args[0], args[1]), + privateGetSenderForTags: () => oracle.aztec_prv_getSenderForTags(), + privateSetSenderForTags: (...args: ACVMField[][]) => oracle.aztec_prv_setSenderForTags(args[0]), + utilityGetUtilityContext: () => oracle.aztec_utl_getUtilityContext(), + utilityStorageRead: (...args: ACVMField[][]) => oracle.aztec_utl_storageRead(args[0], args[1], args[2], args[3]), + utilityStoreCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_storeCapsule(args[0], args[1], args[2]), + utilityCopyCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_copyCapsule(args[0], args[1], args[2], args[3]), + utilityDeleteCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_deleteCapsule(args[0], args[1]), + utilityAes128Decrypt: (...args: ACVMField[][]) => + oracle.aztec_utl_aes128Decrypt(args[0], args[1], args[2], args[3]), + utilityGetSharedSecret: (...args: ACVMField[][]) => + oracle.aztec_utl_getSharedSecret(args[0], args[1], args[2], args[3]), + utilityFetchTaggedLogs: (...args: ACVMField[][]) => oracle.aztec_utl_fetchTaggedLogs(args[0]), + utilityBulkRetrieveLogs: (...args: ACVMField[][]) => oracle.aztec_utl_bulkRetrieveLogs(args[0], args[1], args[2]), + // Adapter: old 3-param signature → new 5-param with injected constants. + // Values derived from: MAX_MESSAGE_CONTENT_LEN(11) - RESERVED_FIELDS (3 for notes, 1 for events). + utilityValidateAndStoreEnqueuedNotesAndEvents: ( + contractAddress: ACVMField[], + noteValidationRequestsArrayBaseSlot: ACVMField[], + eventValidationRequestsArrayBaseSlot: ACVMField[], + ) => + oracle.aztec_utl_validateAndStoreEnqueuedNotesAndEvents( + contractAddress, + noteValidationRequestsArrayBaseSlot, + eventValidationRequestsArrayBaseSlot, + [new Fr(8).toString()], + [new Fr(10).toString()], + ), + utilityGetL1ToL2MembershipWitness: (...args: ACVMField[][]) => + oracle.aztec_utl_getL1ToL2MembershipWitness(args[0], args[1], args[2]), + utilityCheckNullifierExists: (...args: ACVMField[][]) => oracle.aztec_utl_checkNullifierExists(args[0]), + utilityGetRandomField: () => oracle.aztec_utl_getRandomField(), + utilityEmitOffchainEffect: (...args: ACVMField[][]) => oracle.aztec_utl_emitOffchainEffect(args[0]), + // Renames (same signature, different oracle name) + privateNotifySetMinRevertibleSideEffectCounter: (...args: ACVMField[][]) => + oracle.aztec_prv_notifyRevertiblePhaseStart(args[0]), + privateIsSideEffectCounterRevertible: (...args: ACVMField[][]) => oracle.aztec_prv_inRevertiblePhase(args[0]), + // Signature changes: old 4-param oracles → new 1-param validatePublicCalldata + privateNotifyEnqueuedPublicFunctionCall: ( + [_contractAddress]: ACVMField[], + [calldataHash]: ACVMField[], + [_sideEffectCounter]: ACVMField[], + [_isStaticCall]: ACVMField[], + ) => oracle.aztec_prv_validatePublicCalldata([calldataHash]), + privateNotifySetPublicTeardownFunctionCall: ( + [_contractAddress]: ACVMField[], + [calldataHash]: ACVMField[], + [_sideEffectCounter]: ACVMField[], + [_isStaticCall]: ACVMField[], + ) => oracle.aztec_prv_validatePublicCalldata([calldataHash]), + }; +} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index ebfc888b4eab..e1c27aa538e3 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -16,6 +16,7 @@ import { BlockHash } from '@aztec/stdlib/block'; import { ContractClassLog, ContractClassLogFields } from '@aztec/stdlib/logs'; import type { IMiscOracle, IPrivateExecutionOracle, IUtilityExecutionOracle } from './interfaces.js'; +import { buildLegacyOracleCallbacks } from './legacy_oracle_mappings.js'; import { packAsHintedNote } from './note_packing_utils.js'; export class UnavailableOracleError extends Error { @@ -90,10 +91,20 @@ export class Oracle { acc[name] = method.bind(this); return acc; }, {} as ACIRCallback); +<<<<<<< HEAD } utilityAssertCompatibleOracleVersion([version]: ACVMField[]) { this.handlerAsMisc().utilityAssertCompatibleOracleVersion(Fr.fromString(version).toNumber()); +======= + + return { ...callback, ...buildLegacyOracleCallbacks(this) }; + } + + // eslint-disable-next-line camelcase + aztec_utl_assertCompatibleOracleVersion([version]: ACVMField[]) { + this.handlerAsMisc().assertCompatibleOracleVersion(Fr.fromString(version).toNumber()); +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) return Promise.resolve([]); } @@ -102,6 +113,7 @@ export class Oracle { return Promise.resolve([toACVMField(val)]); } +<<<<<<< HEAD privateStoreInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { this.handlerAsPrivate().privateStoreInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); return Promise.resolve([]); @@ -109,6 +121,17 @@ export class Oracle { async privateLoadFromExecutionCache([returnsHash]: ACVMField[]): Promise { const values = await this.handlerAsPrivate().privateLoadFromExecutionCache(Fr.fromString(returnsHash)); +======= + // eslint-disable-next-line camelcase + aztec_prv_storeInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { + this.handlerAsPrivate().storeInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + async aztec_prv_loadFromExecutionCache([returnsHash]: ACVMField[]): Promise { + const values = await this.handlerAsPrivate().loadFromExecutionCache(Fr.fromString(returnsHash)); +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) return [values.map(toACVMField)]; } @@ -417,7 +440,12 @@ export class Oracle { return Promise.resolve([]); } +<<<<<<< HEAD async utilityLog( +======= + // eslint-disable-next-line camelcase + async aztec_utl_log( +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) level: ACVMField[], message: ACVMField[], _ignoredFieldsSize: ACVMField[], @@ -548,7 +576,12 @@ export class Oracle { return []; } +<<<<<<< HEAD async utilityLoadCapsule( +======= + // eslint-disable-next-line camelcase + async aztec_utl_loadCapsule( +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 9e0d38befcba..ec0aa0ccffc0 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -6,6 +6,10 @@ import { Point } from '@aztec/foundation/curves/grumpkin'; import { LogLevels, type Logger, createLogger } from '@aztec/foundation/log'; import type { MembershipWitness } from '@aztec/foundation/trees'; import type { KeyStore } from '@aztec/key-store'; +<<<<<<< HEAD +======= +import { isProtocolContract } from '@aztec/protocol-contracts'; +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; @@ -108,7 +112,26 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.scopes = args.scopes; } +<<<<<<< HEAD public utilityAssertCompatibleOracleVersion(version: number): void { +======= + public assertCompatibleOracleVersion(version: number): void { + // TODO(F-416): Remove this hack on v5 when protocol contracts are redeployed. + // Protocol contracts/canonical contracts shipped with committed bytecode that cannot be changed. Assert they use + // the expected pinned version or the current one. We want to allow for both the pinned and the current versions + // because we want this code to work with both the pinned and unpinned version since some branches do not have the + // pinned contracts (like e.g. next) + const LEGACY_ORACLE_VERSION = 12; + if (isProtocolContract(this.contractAddress)) { + if (version !== LEGACY_ORACLE_VERSION && version !== ORACLE_VERSION) { + throw new Error( + `Expected legacy oracle version ${LEGACY_ORACLE_VERSION} or current oracle version ${ORACLE_VERSION} for alpha payload contract at ${this.contractAddress}, got ${version}.`, + ); + } + return; + } + +>>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) if (version !== ORACLE_VERSION) { throw new Error(`Incompatible oracle version. Expected version ${ORACLE_VERSION}, got ${version}.`); } From 913f3d0945dc08009b309b867cdd9b221112cff9 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Thu, 12 Mar 2026 22:57:10 +0000 Subject: [PATCH 7/7] fix: resolve cherry-pick conflicts for v4-next Adapted oracle method names to v4-next convention (utilityXxx/privateXxx instead of aztec_utl_/aztec_prv_ used on next). Removed legacy mappings for #21209 renames and signature changes that haven't landed on v4-next. --- .../oracle/legacy_oracle_mappings.ts | 93 +++++++------------ .../oracle/oracle.ts | 36 +------ .../oracle/utility_execution_oracle.ts | 7 -- 3 files changed, 38 insertions(+), 98 deletions(-) diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts index ee95befc339a..848198928008 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_mappings.ts @@ -1,4 +1,3 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; import type { ACIRCallback, ACVMField } from '@aztec/simulator/client'; import type { Oracle } from './oracle.js'; @@ -7,71 +6,49 @@ import type { Oracle } from './oracle.js'; * Builds legacy oracle name callbacks for pinned protocol contracts whose artifacts are committed and cannot be * changed. * TODO(F-416): Remove these aliases on v5 when protocol contracts are redeployed. + * + * NOTE: On v4-next the oracle methods have NOT been renamed to aztec_utl_/aztec_prv_ prefixes yet, so these + * entries map old names to the same old names (which are the current method names). They are kept for + * forward-compatibility in case the rename lands on this branch. */ export function buildLegacyOracleCallbacks(oracle: Oracle): ACIRCallback { return { - // Simple prefix renames (privateXxx/utilityXxx → aztec_prv_/aztec_utl_) - utilityLog: (...args: ACVMField[][]) => oracle.aztec_utl_log(args[0], args[1], args[2], args[3]), + // Simple prefix renames (on v4-next these are identity mappings since methods haven't been renamed) + utilityLog: (...args: ACVMField[][]) => oracle.utilityLog(args[0], args[1], args[2], args[3]), utilityAssertCompatibleOracleVersion: (...args: ACVMField[][]) => - oracle.aztec_utl_assertCompatibleOracleVersion(args[0]), - utilityLoadCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_loadCapsule(args[0], args[1], args[2]), - privateStoreInExecutionCache: (...args: ACVMField[][]) => oracle.aztec_prv_storeInExecutionCache(args[0], args[1]), - privateLoadFromExecutionCache: (...args: ACVMField[][]) => oracle.aztec_prv_loadFromExecutionCache(args[0]), + oracle.utilityAssertCompatibleOracleVersion(args[0]), + utilityLoadCapsule: (...args: ACVMField[][]) => oracle.utilityLoadCapsule(args[0], args[1], args[2]), + privateStoreInExecutionCache: (...args: ACVMField[][]) => oracle.privateStoreInExecutionCache(args[0], args[1]), + privateLoadFromExecutionCache: (...args: ACVMField[][]) => oracle.privateLoadFromExecutionCache(args[0]), privateCallPrivateFunction: (...args: ACVMField[][]) => - oracle.aztec_prv_callPrivateFunction(args[0], args[1], args[2], args[3], args[4]), - privateIsNullifierPending: (...args: ACVMField[][]) => oracle.aztec_prv_isNullifierPending(args[0], args[1]), - privateNotifyCreatedNullifier: (...args: ACVMField[][]) => oracle.aztec_prv_notifyCreatedNullifier(args[0]), + oracle.privateCallPrivateFunction(args[0], args[1], args[2], args[3], args[4]), + privateIsNullifierPending: (...args: ACVMField[][]) => oracle.privateIsNullifierPending(args[0], args[1]), + privateNotifyCreatedNullifier: (...args: ACVMField[][]) => oracle.privateNotifyCreatedNullifier(args[0]), privateNotifyCreatedContractClassLog: (...args: ACVMField[][]) => - oracle.aztec_prv_notifyCreatedContractClassLog(args[0], args[1], args[2], args[3]), - privateGetNextAppTagAsSender: (...args: ACVMField[][]) => oracle.aztec_prv_getNextAppTagAsSender(args[0], args[1]), - privateGetSenderForTags: () => oracle.aztec_prv_getSenderForTags(), - privateSetSenderForTags: (...args: ACVMField[][]) => oracle.aztec_prv_setSenderForTags(args[0]), - utilityGetUtilityContext: () => oracle.aztec_utl_getUtilityContext(), - utilityStorageRead: (...args: ACVMField[][]) => oracle.aztec_utl_storageRead(args[0], args[1], args[2], args[3]), - utilityStoreCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_storeCapsule(args[0], args[1], args[2]), - utilityCopyCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_copyCapsule(args[0], args[1], args[2], args[3]), - utilityDeleteCapsule: (...args: ACVMField[][]) => oracle.aztec_utl_deleteCapsule(args[0], args[1]), + oracle.privateNotifyCreatedContractClassLog(args[0], args[1], args[2], args[3]), + privateGetNextAppTagAsSender: (...args: ACVMField[][]) => oracle.privateGetNextAppTagAsSender(args[0], args[1]), + privateGetSenderForTags: () => oracle.privateGetSenderForTags(), + privateSetSenderForTags: (...args: ACVMField[][]) => oracle.privateSetSenderForTags(args[0]), + utilityGetUtilityContext: () => oracle.utilityGetUtilityContext(), + utilityStorageRead: (...args: ACVMField[][]) => oracle.utilityStorageRead(args[0], args[1], args[2], args[3]), + utilityStoreCapsule: (...args: ACVMField[][]) => oracle.utilityStoreCapsule(args[0], args[1], args[2]), + utilityCopyCapsule: (...args: ACVMField[][]) => oracle.utilityCopyCapsule(args[0], args[1], args[2], args[3]), + utilityDeleteCapsule: (...args: ACVMField[][]) => oracle.utilityDeleteCapsule(args[0], args[1]), utilityAes128Decrypt: (...args: ACVMField[][]) => - oracle.aztec_utl_aes128Decrypt(args[0], args[1], args[2], args[3]), + oracle.utilityAes128Decrypt(args[0], args[1], args[2], args[3]), utilityGetSharedSecret: (...args: ACVMField[][]) => - oracle.aztec_utl_getSharedSecret(args[0], args[1], args[2], args[3]), - utilityFetchTaggedLogs: (...args: ACVMField[][]) => oracle.aztec_utl_fetchTaggedLogs(args[0]), - utilityBulkRetrieveLogs: (...args: ACVMField[][]) => oracle.aztec_utl_bulkRetrieveLogs(args[0], args[1], args[2]), - // Adapter: old 3-param signature → new 5-param with injected constants. - // Values derived from: MAX_MESSAGE_CONTENT_LEN(11) - RESERVED_FIELDS (3 for notes, 1 for events). - utilityValidateAndStoreEnqueuedNotesAndEvents: ( - contractAddress: ACVMField[], - noteValidationRequestsArrayBaseSlot: ACVMField[], - eventValidationRequestsArrayBaseSlot: ACVMField[], - ) => - oracle.aztec_utl_validateAndStoreEnqueuedNotesAndEvents( - contractAddress, - noteValidationRequestsArrayBaseSlot, - eventValidationRequestsArrayBaseSlot, - [new Fr(8).toString()], - [new Fr(10).toString()], - ), + oracle.utilityGetSharedSecret(args[0], args[1], args[2], args[3]), + utilityFetchTaggedLogs: (...args: ACVMField[][]) => oracle.utilityFetchTaggedLogs(args[0]), + utilityBulkRetrieveLogs: (...args: ACVMField[][]) => oracle.utilityBulkRetrieveLogs(args[0], args[1], args[2]), + utilityValidateAndStoreEnqueuedNotesAndEvents: (...args: ACVMField[][]) => + oracle.utilityValidateAndStoreEnqueuedNotesAndEvents(args[0], args[1], args[2]), utilityGetL1ToL2MembershipWitness: (...args: ACVMField[][]) => - oracle.aztec_utl_getL1ToL2MembershipWitness(args[0], args[1], args[2]), - utilityCheckNullifierExists: (...args: ACVMField[][]) => oracle.aztec_utl_checkNullifierExists(args[0]), - utilityGetRandomField: () => oracle.aztec_utl_getRandomField(), - utilityEmitOffchainEffect: (...args: ACVMField[][]) => oracle.aztec_utl_emitOffchainEffect(args[0]), - // Renames (same signature, different oracle name) - privateNotifySetMinRevertibleSideEffectCounter: (...args: ACVMField[][]) => - oracle.aztec_prv_notifyRevertiblePhaseStart(args[0]), - privateIsSideEffectCounterRevertible: (...args: ACVMField[][]) => oracle.aztec_prv_inRevertiblePhase(args[0]), - // Signature changes: old 4-param oracles → new 1-param validatePublicCalldata - privateNotifyEnqueuedPublicFunctionCall: ( - [_contractAddress]: ACVMField[], - [calldataHash]: ACVMField[], - [_sideEffectCounter]: ACVMField[], - [_isStaticCall]: ACVMField[], - ) => oracle.aztec_prv_validatePublicCalldata([calldataHash]), - privateNotifySetPublicTeardownFunctionCall: ( - [_contractAddress]: ACVMField[], - [calldataHash]: ACVMField[], - [_sideEffectCounter]: ACVMField[], - [_isStaticCall]: ACVMField[], - ) => oracle.aztec_prv_validatePublicCalldata([calldataHash]), + oracle.utilityGetL1ToL2MembershipWitness(args[0], args[1], args[2]), + utilityCheckNullifierExists: (...args: ACVMField[][]) => oracle.utilityCheckNullifierExists(args[0]), + utilityGetRandomField: () => oracle.utilityGetRandomField(), + utilityEmitOffchainEffect: (...args: ACVMField[][]) => oracle.utilityEmitOffchainEffect(args[0]), + // On v4-next, the #21209 renames (privateNotifySetMinRevertibleSideEffectCounter → + // notifyRevertiblePhaseStart, etc.) and signature changes (4-param → 1-param validatePublicCalldata) + // have NOT landed, so those legacy mappings are not needed here. }; } diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index e1c27aa538e3..72a5c75177a4 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -86,25 +86,17 @@ export class Oracle { }); // Build callback object and return it - return oracleNames.reduce((acc, name) => { + const callback = oracleNames.reduce((acc, name) => { const method = this[name as keyof Omit]; acc[name] = method.bind(this); return acc; }, {} as ACIRCallback); -<<<<<<< HEAD - } - - utilityAssertCompatibleOracleVersion([version]: ACVMField[]) { - this.handlerAsMisc().utilityAssertCompatibleOracleVersion(Fr.fromString(version).toNumber()); -======= return { ...callback, ...buildLegacyOracleCallbacks(this) }; } - // eslint-disable-next-line camelcase - aztec_utl_assertCompatibleOracleVersion([version]: ACVMField[]) { - this.handlerAsMisc().assertCompatibleOracleVersion(Fr.fromString(version).toNumber()); ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) + utilityAssertCompatibleOracleVersion([version]: ACVMField[]) { + this.handlerAsMisc().utilityAssertCompatibleOracleVersion(Fr.fromString(version).toNumber()); return Promise.resolve([]); } @@ -113,7 +105,6 @@ export class Oracle { return Promise.resolve([toACVMField(val)]); } -<<<<<<< HEAD privateStoreInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { this.handlerAsPrivate().privateStoreInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); return Promise.resolve([]); @@ -121,17 +112,6 @@ export class Oracle { async privateLoadFromExecutionCache([returnsHash]: ACVMField[]): Promise { const values = await this.handlerAsPrivate().privateLoadFromExecutionCache(Fr.fromString(returnsHash)); -======= - // eslint-disable-next-line camelcase - aztec_prv_storeInExecutionCache(values: ACVMField[], [hash]: ACVMField[]): Promise { - this.handlerAsPrivate().storeInExecutionCache(values.map(Fr.fromString), Fr.fromString(hash)); - return Promise.resolve([]); - } - - // eslint-disable-next-line camelcase - async aztec_prv_loadFromExecutionCache([returnsHash]: ACVMField[]): Promise { - const values = await this.handlerAsPrivate().loadFromExecutionCache(Fr.fromString(returnsHash)); ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) return [values.map(toACVMField)]; } @@ -440,12 +420,7 @@ export class Oracle { return Promise.resolve([]); } -<<<<<<< HEAD async utilityLog( -======= - // eslint-disable-next-line camelcase - async aztec_utl_log( ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) level: ACVMField[], message: ACVMField[], _ignoredFieldsSize: ACVMField[], @@ -576,12 +551,7 @@ export class Oracle { return []; } -<<<<<<< HEAD async utilityLoadCapsule( -======= - // eslint-disable-next-line camelcase - async aztec_utl_loadCapsule( ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) [contractAddress]: ACVMField[], [slot]: ACVMField[], [tSize]: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index ec0aa0ccffc0..384908be7bae 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -6,10 +6,7 @@ import { Point } from '@aztec/foundation/curves/grumpkin'; import { LogLevels, type Logger, createLogger } from '@aztec/foundation/log'; import type { MembershipWitness } from '@aztec/foundation/trees'; import type { KeyStore } from '@aztec/key-store'; -<<<<<<< HEAD -======= import { isProtocolContract } from '@aztec/protocol-contracts'; ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; @@ -112,10 +109,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.scopes = args.scopes; } -<<<<<<< HEAD public utilityAssertCompatibleOracleVersion(version: number): void { -======= - public assertCompatibleOracleVersion(version: number): void { // TODO(F-416): Remove this hack on v5 when protocol contracts are redeployed. // Protocol contracts/canonical contracts shipped with committed bytecode that cannot be changed. Assert they use // the expected pinned version or the current one. We want to allow for both the pinned and the current versions @@ -131,7 +125,6 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return; } ->>>>>>> 1019f2a65a (fix: complete legacy oracle mappings for all pinned contracts (#21404)) if (version !== ORACLE_VERSION) { throw new Error(`Incompatible oracle version. Expected version ${ORACLE_VERSION}, got ${version}.`); }