diff --git a/yarn-project/archiver/src/store/log_store.test.ts b/yarn-project/archiver/src/store/log_store.test.ts index 1b684535f063..959390805a28 100644 --- a/yarn-project/archiver/src/store/log_store.test.ts +++ b/yarn-project/archiver/src/store/log_store.test.ts @@ -5,7 +5,14 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash, GENESIS_BLOCK_HEADER_HASH } from '@aztec/stdlib/block'; import { Checkpoint, type PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import { MAX_LOGS_PER_TAG } from '@aztec/stdlib/interfaces/api-limit'; -import { LogCursor, SiloedTag, Tag, queryAllPrivateLogsByTags, queryAllPublicLogsByTags } from '@aztec/stdlib/logs'; +import { + LogCursor, + PublicLog, + SiloedTag, + Tag, + queryAllPrivateLogsByTags, + queryAllPublicLogsByTags, +} from '@aztec/stdlib/logs'; import '@aztec/stdlib/testing/jest'; import type { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; @@ -99,6 +106,56 @@ describe('LogStore', () => { const after = await logStore.getPrivateLogsByTags({ tags: [tag] }); expect(after[0].length).toBe(0); }); + + it('ingests a zero-field public log without throwing and omits it from tagged lookup', async () => { + const ckpt = await makeCheckpointWithLogs(1, { + numTxsPerBlock: 1, + publicLogs: { numLogsPerTx: 1, contractAddress: CONTRACT }, + }); + const block = ckpt.checkpoint.blocks[0]; + // A protocol-valid public log can carry zero fields (raw AVM EMITPUBLICLOG with logSize=0); it has + // no tag. Previously fieldHex(fields[0]) read off an empty array and aborted the whole store txn. + block.body.txEffects[0].publicLogs[0] = new PublicLog(CONTRACT, []); + await blockStore.addProposedBlock(block); + + await expect(logStore.addLogs([block])).resolves.toBe(true); + + // The untagged log is still retrievable via the per-block read... + const pub = await logStore.getPublicLogsForBlock(block.number); + expect(pub.length).toBe(1); + expect(pub[0].logData).toEqual([]); + + // ...but a real (64-hex-char) tag query never matches it. + const [byTag] = await logStore.getPublicLogsByTags({ contractAddress: CONTRACT, tags: [new Tag(Fr.ZERO)] }); + expect(byTag).toEqual([]); + }); + + it('prunes a zero-field public log alongside normal logs (empty-tag key tracked for deletion)', async () => { + const ckpt = await makeCheckpointWithLogs(1, { + numTxsPerBlock: 1, + publicLogs: { numLogsPerTx: 2, contractAddress: CONTRACT }, + }); + const block = ckpt.checkpoint.blocks[0]; + // First public log carries zero fields (no tag, indexed under the empty tag); the second keeps a + // normal tagged form. Both must be dropped when the block is pruned on reorg. + block.body.txEffects[0].publicLogs[0] = new PublicLog(CONTRACT, []); + const normalTag = new Tag(new Fr(0xc0ffee)); + block.body.txEffects[0].publicLogs[1].fields[0] = normalTag.value; + await blockStore.addProposedBlock(block); + await logStore.addLogs([block]); + + // Both logs are indexed for the block, and the tagged one is queryable. + expect((await logStore.getPublicLogsForBlock(block.number)).length).toBe(2); + const [taggedBefore] = await logStore.getPublicLogsByTags({ contractAddress: CONTRACT, tags: [normalTag] }); + expect(taggedBefore.length).toBe(1); + + await logStore.deleteLogs([block]); + + // The reorg trim drops every key recorded for the block — including the empty-tag one. + expect(await logStore.getPublicLogsForBlock(block.number)).toEqual([]); + const [taggedAfter] = await logStore.getPublicLogsByTags({ contractAddress: CONTRACT, tags: [normalTag] }); + expect(taggedAfter).toEqual([]); + }); }); describe('getPrivateLogsByTags', () => { diff --git a/yarn-project/archiver/src/store/log_store.ts b/yarn-project/archiver/src/store/log_store.ts index 49d4d719084a..9c03b504a1ee 100644 --- a/yarn-project/archiver/src/store/log_store.ts +++ b/yarn-project/archiver/src/store/log_store.ts @@ -26,6 +26,7 @@ import { endOfTxRange, fieldHex, incKey, + tagHexForLog, } from './log_store_codec.js'; /** @@ -100,7 +101,7 @@ export class LogStore { let publicLogIndexWithinTx = 0; for (const log of txEffect.privateLogs) { - const tagHex = fieldHex(log.fields[0]); + const tagHex = tagHexForLog(log.fields); const key = encodeKey(tagHex, blockNumber, txIndexWithinBlock, privateLogIndexWithinTx); const value = encodeValue({ txHash, @@ -115,7 +116,7 @@ export class LogStore { for (const log of txEffect.publicLogs) { const contractHex = fieldHex(log.contractAddress); - const tagHex = fieldHex(log.fields[0]); + const tagHex = tagHexForLog(log.fields); const key = encodeKey( encodePublicPrefix(contractHex, tagHex), blockNumber, diff --git a/yarn-project/archiver/src/store/log_store_codec.ts b/yarn-project/archiver/src/store/log_store_codec.ts index 0a1edfc8bdb2..6af7a898ea63 100644 --- a/yarn-project/archiver/src/store/log_store_codec.ts +++ b/yarn-project/archiver/src/store/log_store_codec.ts @@ -37,6 +37,17 @@ export function fieldHex(value: Fr | { toString: () => string }): string { return value.toString().slice(2); } +/** + * Tag prefix for a log: the hex of its first field, or the empty string when the log carries no fields. + * A protocol-valid public log can have zero fields (e.g. a raw AVM `EMITPUBLICLOG` with `logSize = 0`), + * which has no tag to index by. Encoding it under the empty tag keeps it retrievable via the per-block + * read (and droppable on reorg) while never matching a real 64-hex-char tag query — instead of reading + * `fields[0]` off an empty array and aborting the whole block-ingestion transaction. + */ +export function tagHexForLog(fields: Fr[]): string { + return fields.length > 0 ? fieldHex(fields[0]) : ''; +} + /** Encodes a number as 8-char zero-padded lowercase hex (matches a u32 big-endian byte buffer's lex order). */ export function u32Hex(n: number): string { return n.toString(16).padStart(NUMERIC_HEX_LEN, '0'); diff --git a/yarn-project/end-to-end/src/bench/bench_build_block.test.ts b/yarn-project/end-to-end/src/bench/bench_build_block.test.ts index c0d4d0ccfbf2..c491b470fef8 100644 --- a/yarn-project/end-to-end/src/bench/bench_build_block.test.ts +++ b/yarn-project/end-to-end/src/bench/bench_build_block.test.ts @@ -10,6 +10,9 @@ const ETHEREUM_SLOT_DURATION_SECONDS = 12; const BLOCK_DURATION_MS = 200_000; const L1_TX_TIMEOUT_MS = 30 * 60 * 1000; +// Block-building latency benchmark. Uses benchmarkSetup() (wraps setup() with telemetry override) and +// emits BENCH_OUTPUT JSON for the GitHub Benchmark Action. Measures sequencer block-build duration and +// mana throughput across 32-tx standard and 8-tx compute-heavy block configurations. describe('benchmarks/build_block', () => { let context: EndToEndContext; let contract: BenchmarkingContract; diff --git a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts index d87034a430f0..3f572cca0192 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts @@ -15,6 +15,9 @@ import { type AccountType, type BenchmarkingFeePaymentMethod, ClientFlowsBenchma jest.setTimeout(300_000); +// Account deployment round-trip benchmark. Uses ClientFlowsBenchmark (wraps setup()) with BENCHMARK_CONFIG +// env var; profiles the full deployment flow (simulate → prove → send → wait) for ECDSA-R1 and Schnorr +// account types with various fee-payment methods. Bench pipeline only. describe('Deployment benchmark', () => { const t = new ClientFlowsBenchmark('deployments'); diff --git a/yarn-project/end-to-end/src/bench/client_flows/amm.test.ts b/yarn-project/end-to-end/src/bench/client_flows/amm.test.ts index bceb8cc97dc3..5ca8a5003776 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/amm.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/amm.test.ts @@ -38,6 +38,8 @@ interface RoundTripData { roundTrips: RoundTripStats | undefined; } +// AMM interaction round-trip benchmark. Uses ClientFlowsBenchmark with BENCHMARK_CONFIG; profiles +// add-liquidity and swap flows across account types and fee-payment methods; emits BENCH_OUTPUT JSON. describe('AMM benchmark', () => { const roundTripData: RoundTripData[] = []; const t = new ClientFlowsBenchmark('amm'); diff --git a/yarn-project/end-to-end/src/bench/client_flows/bridging.test.ts b/yarn-project/end-to-end/src/bench/client_flows/bridging.test.ts index 60bfea7a942a..7e504e0ccc12 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/bridging.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/bridging.test.ts @@ -13,6 +13,8 @@ import { type AccountType, type BenchmarkingFeePaymentMethod, ClientFlowsBenchma jest.setTimeout(300_000); +// L1↔L2 bridging round-trip benchmark. Uses ClientFlowsBenchmark (wraps CrossChainTestHarness) with +// BENCHMARK_CONFIG; profiles the full bridge-in flow for multiple account/fee-method combinations. describe('Bridging benchmark', () => { const t = new ClientFlowsBenchmark('bridging'); // The wallet used by the user to interact diff --git a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts index 2e8ea2765a7f..f16880325953 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts @@ -13,6 +13,8 @@ import { type AccountType, type BenchmarkingFeePaymentMethod, ClientFlowsBenchma jest.setTimeout(1_600_000); +// Contract deployment round-trip benchmark. Uses ClientFlowsBenchmark with BENCHMARK_CONFIG; profiles +// PrivateVoting contract deployment across account types and fee-payment methods; emits BENCH_OUTPUT JSON. describe('Deployment benchmark', () => { const t = new ClientFlowsBenchmark('deployments'); let node: AztecNode; diff --git a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts index 9dd70af114e7..7dff13c7454c 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts @@ -17,6 +17,8 @@ import { type AccountType, type BenchmarkingFeePaymentMethod, ClientFlowsBenchma jest.setTimeout(300_000); +// Storage proof round-trip benchmark. Uses ClientFlowsBenchmark with BENCHMARK_CONFIG; profiles the full +// buildStorageProofCapsules + contract-call flow for multiple account/fee-method combinations. describe('Storage proof benchmark', () => { const t = new ClientFlowsBenchmark('storage_proof'); let userWallet: TestWallet; diff --git a/yarn-project/end-to-end/src/bench/client_flows/transfers.test.ts b/yarn-project/end-to-end/src/bench/client_flows/transfers.test.ts index ba224de0e1cc..af7a9cdffa20 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/transfers.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/transfers.test.ts @@ -19,6 +19,8 @@ const AMOUNT_PER_NOTE = 1_000_000; const MINIMUM_NOTES_FOR_RECURSION_LEVEL = [0, 2, 10]; +// Token transfer round-trip benchmark. Uses ClientFlowsBenchmark with BENCHMARK_CONFIG; profiles private +// token transfer flows at varying note-recursion depths for multiple account/fee-method combinations. describe('Transfer benchmark', () => { const t = new ClientFlowsBenchmark('transfers'); // The wallet used by the admin to interact diff --git a/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts b/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts index 5509513ec15f..098e033ad634 100644 --- a/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts +++ b/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts @@ -95,6 +95,9 @@ async function benchmark( }; } +// Node RPC performance benchmark. Uses setup() with PIPELINING_SETUP_OPTS, builds BLOCKS_TO_BUILD blocks, +// then iterates all RPC endpoints measuring avg/min/max latency; emits BENCH_OUTPUT JSON for the bench +// pipeline. Not in test_cmds; runs via bench_cmds. describe('e2e_node_rpc_perf', () => { jest.setTimeout(10 * 60 * 1000); // 10 minutes diff --git a/yarn-project/end-to-end/src/bench/tx_stats_bench.test.ts b/yarn-project/end-to-end/src/bench/tx_stats_bench.test.ts index 4b0fda236914..64083b2430ca 100644 --- a/yarn-project/end-to-end/src/bench/tx_stats_bench.test.ts +++ b/yarn-project/end-to-end/src/bench/tx_stats_bench.test.ts @@ -27,6 +27,9 @@ import { proveInteraction } from '../test-wallet/utils.js'; const REAL_PROOFS = !parseBooleanEnv(process.env.FAKE_PROOFS); const TIMEOUT = REAL_PROOFS ? 45 * 60 * 1000 : 15 * 60 * 1000; +// Transaction stats benchmark. Uses FullProverTest with real or fake proofs (FAKE_PROOFS env var). +// Measures proof generation time, tx wire size, and compression ratios (snappy/brotli/zstd) for public +// and private transactions; emits BENCH_OUTPUT JSON. Bench pipeline only. describe('transaction benchmarks', () => { const COINBASE_ADDRESS = EthAddress.random(); const t = new FullProverTest('full_prover', 1, COINBASE_ADDRESS, REAL_PROOFS); diff --git a/yarn-project/end-to-end/src/composed/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/composed/e2e_cheat_codes.test.ts index 6febfdf40ca8..22ccae679bb9 100644 --- a/yarn-project/end-to-end/src/composed/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_cheat_codes.test.ts @@ -9,6 +9,7 @@ const { AZTEC_NODE_URL = 'http://localhost:8080', ETHEREUM_HOSTS = 'http://local // Unlike the non-composed e2e_cheat_codes.test.ts these tests are testing that the AztecNodeDebug endpoints get // correctly exposed on the node. +// Runs against a pre-started docker-compose network (AZTEC_NODE_URL + ETHEREUM_HOSTS); no in-proc setup(). describe('e2e_cheat_codes', () => { const logger = createLogger('e2e:cheat_codes'); let aztecNode: AztecNode; 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 e02fe9dcf496..a86bbdea41fc 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 @@ -32,6 +32,9 @@ const { AZTEC_NODE_URL = 'http://localhost:8080' } = process.env; // // 3. Run the tests: // yarn test:e2e e2e_local_network_example.test.ts +// End-to-end example of connecting to the --local-network quickstart. Runs against a pre-started +// docker-compose stack (AZTEC_NODE_URL); demonstrates account loading, token deployment, and transfers +// using only the public aztec.js npm API. describe('e2e_local_network_example', () => { it('local network example works', async () => { ////////////// CREATE THE CLIENT INTERFACE AND CONTACT THE LOCAL NETWORK ////////////// diff --git a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts index 04c20b23ca60..2073a2b6b710 100644 --- a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts @@ -22,6 +22,9 @@ import type { TestWallet } from '../test-wallet/test_wallet.js'; jest.setTimeout(15 * 60 * 1000); +// Node and PXE persistence tests. Uses setup() directly with PIPELINING_SETUP_OPTS; excluded from the +// compose glob for unknown reasons (migrate-later candidate). Spawns and tears down node/PXE with +// varying combinations of persisted vs empty data directories to cover five restart scenarios. describe('Aztec persistence', () => { /** * These tests check that the Aztec Node and PXE can be shutdown and restarted without losing data. diff --git a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts index f398e8f38ece..001b9eb7ca08 100644 --- a/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_token_bridge_tutorial_test.test.ts @@ -86,6 +86,9 @@ async function addMinter(l1TokenContract: EthAddress, l1TokenHandler: EthAddress // // 3. Run the tests: // yarn test:e2e e2e_token_bridge_tutorial_test.test.ts +// Token bridge tutorial test. Runs against a pre-started local network (AZTEC_NODE_URL + ETHEREUM_HOSTS) +// using only published npm packages. Deploys an L1 ERC20/portal and L2 token bridge, then exercises the +// full L1↔L2 bridging flow. Intentional constraint: no in-proc setup(). describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { it('Deploys tokens & bridges to L1 & L2, mints & publicly bridges tokens', async () => { const logger = createLogger('aztec:token-bridge-tutorial'); diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts index 033807fb63fc..9bcc9288c104 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts @@ -88,6 +88,9 @@ async function waitForTriggerTx(node: AztecNode, txHash: TxHash): Promise { jest.setTimeout(20 * 60 * 1000); // 20 minutes diff --git a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts index df10a3bd67d0..f682cba794fc 100644 --- a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts @@ -24,6 +24,9 @@ import { getLogger, startAnvil } from '../fixtures/utils.js'; * Regenerate this test's fixture with * AZTEC_GENERATE_TEST_DATA=1 yarn workspace @aztec/prover-client test bb_prover_full_rollup */ +// Standalone Honk proof verifier integration test. Starts its own anvil, deploys a HonkVerifier contract, +// loads a serialised RootRollupPublicInputs fixture, and verifies the proof on-chain via BBCircuitVerifier. +// No Aztec node. Excluded from compose glob; requires a pre-generated proof fixture (AZTEC_GENERATE_TEST_DATA). describe('proof_verification', () => { let proof: Proof; let publicInputs: RootRollupPublicInputs; diff --git a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts index a85c1b4988b9..bf0a35089461 100644 --- a/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/composed/uniswap_trade_on_l1_from_l2.test.ts @@ -4,6 +4,9 @@ import { uniswapL1L2TestSuite } from '../shared/uniswap_l1_l2.js'; // This tests works on forked mainnet. There is a dump of the data in `dumpedState` such that we // don't need to burn through RPC requests. +// Uses setup() with PIPELINING_SETUP_OPTS, stateLoad (anvil chain dump), and startProverNode. Delegates to +// uniswapL1L2TestSuite which drives L1 Uniswap interactions from L2. Migrate-later candidate — runs in-proc +// but stateLoad dump and prover node need verification before folding into the standard simple suite. const dumpedState = 'src/fixtures/dumps/uniswap_state'; // When taking a dump use the block number of the fork to improve speed. const EXPECTED_FORKED_BLOCK = 0; //17514288; 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 af674e9dcdad..23c54df05c3b 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 @@ -156,6 +156,9 @@ function verifyKeyStore(directory: string) { jest.setTimeout(10 * 60 * 1000); +// Multi-validator key-store test using a Web3Signer sidecar (docker-compose web3signer suite). Runs +// setup() with PIPELINING_SETUP_OPTS and multiple keystores loaded through the NodeKeystoreAdapter and +// Web3Signer, then verifies that blocks are proposed and proven across VALIDATOR_COUNT validators. describe('e2e_multi_validator_node', () => { let initialValidatorPrivateKeys: `0x${string}`[]; let validatorAddresses: `0x${string}`[]; diff --git a/yarn-project/end-to-end/src/composed/web3signer/integration_remote_signer.test.ts b/yarn-project/end-to-end/src/composed/web3signer/integration_remote_signer.test.ts index c097f849c7cd..f1c9ee2c9f73 100644 --- a/yarn-project/end-to-end/src/composed/web3signer/integration_remote_signer.test.ts +++ b/yarn-project/end-to-end/src/composed/web3signer/integration_remote_signer.test.ts @@ -15,6 +15,9 @@ import { const { L1_CHAIN_ID = '31337' } = process.env; +// Integration test for RemoteSigner against a live Web3Signer instance (docker-compose web3signer suite). +// No Aztec node; exercises EIP-712 typed-data signing and raw transaction signing, comparing remote results +// against a local signer for the same key. describe('RemoteSigner integration: Web3Signer (compose)', () => { jest.setTimeout(180_000); diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index f7e7a904a2f6..08a5bd43db6d 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -17,6 +17,10 @@ import { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 300_000; +// Tests multi-PXE isolation: one node with two separate PXE instances attached, each holding different +// account keys. Exercises note decryption scoping, contract registration ordering, and deferred-note +// reprocessing when a contract is registered late. setup(1, AUTOMINE_E2E_OPTS) provides one node with +// automine sequencer; a second PXE is attached via setupPXEAndGetWallet. describe('e2e_2_pxes', () => { jest.setTimeout(TIMEOUT); @@ -75,6 +79,8 @@ describe('e2e_2_pxes', () => { await teardownA(); }); + // Deploys a Token on PXE A, mints initial balance, transfers A→B via PXE A, then B→A via PXE B. + // Asserts balances visible on each PXE match expectations after each step. it('transfers funds from user A to B via PXE A followed by transfer from B to A via PXE B', async () => { const initialBalance = 987n; const transferAmount1 = 654n; @@ -125,6 +131,8 @@ describe('e2e_2_pxes', () => { const getChildStoredValue = (child: { address: AztecAddress }, node: AztecNode) => node.getPublicStorageAt('latest', child.address, new Fr(1)); + // Deploys a Child contract via PXE A, registers it on PXE B, then calls a public write from PXE B. + // Asserts the stored value is visible through the shared node regardless of which PXE was used. it('user calls a public function on a contract deployed by a different user using a different PXE', async () => { const childCompleteAddress = await deployChildContractViaServerA(); @@ -143,6 +151,8 @@ describe('e2e_2_pxes', () => { expect(storedValueOnA).toEqual(newValueToSet); }); + // Mints private balances for two accounts, each on their own PXE. Verifies that querying + // the balance for account A from PXE B returns 0 (key not registered), and vice versa. it('private state is "zero" when PXE does not have the account secret key', async () => { const userABalance = 100n; const userBBalance = 150n; @@ -167,6 +177,8 @@ describe('e2e_2_pxes', () => { await expectTokenBalance(walletA, token, accountBAddress, 0n, logger); }); + // Transfers tokens to PXE B's account before B has registered the contract. Then B registers + // the contract and asserts it can now see its balance from the deferred notes. it('permits sending funds to a user before they have registered the contract', async () => { const initialBalance = 987n; const transferAmount1 = 654n; @@ -187,6 +199,9 @@ describe('e2e_2_pxes', () => { await expectTokenBalance(walletB, token, accountBAddress, transferAmount1, logger); }); + // A shared account is registered on both PXEs. Tokens are sent through the shared account before + // PXE B registers the contract. Verifies deferred-note reprocessing correctly applies nullifiers + // and reconciles balances after late contract registration. it('permits sending funds to a user, and spending them, before they have registered the contract', async () => { const initialBalance = 987n; const transferAmount1 = 654n; @@ -226,6 +241,8 @@ describe('e2e_2_pxes', () => { await expectTokenBalance(walletB, token, sharedAccountAddress, transferAmount1 - transferAmount2, logger); }); + // Sets up a third PXE (C) that has no knowledge of sender A. Transfers tokens A→C, confirms C + // sees 0 balance. Then registers A as a sender on C and confirms the balance is immediately visible. it('balance updates automatically after sender is registered', async () => { const initialBalance = 500n; const transferAmount = 200n; diff --git a/yarn-project/end-to-end/src/e2e_abi_types.test.ts b/yarn-project/end-to-end/src/e2e_abi_types.test.ts index 81ca7f63667f..dc2b700811d8 100644 --- a/yarn-project/end-to-end/src/e2e_abi_types.test.ts +++ b/yarn-project/end-to-end/src/e2e_abi_types.test.ts @@ -16,8 +16,9 @@ const U64_MAX = 2n ** 64n - 1n; const I64_MAX = 2n ** 63n - 1n; const I64_MIN = -(2n ** 63n); -// Tests that different types are supported to be passed to contract functions and received as return values. This -// mirrors the Noir tests for the AbiTypes contract to make sure that these values can also be passed from TS. +// Tests that different ABI types are correctly encoded when passed to contract functions and decoded from +// return values in TypeScript. Mirrors Noir-side AbiTypes unit tests. Uses setup(1, AUTOMINE_E2E_OPTS) +// providing one node, automine sequencer, and one deployed account. describe('AbiTypes', () => { let abiTypesContract: AbiTypesContract; jest.setTimeout(TIMEOUT); @@ -37,6 +38,8 @@ describe('AbiTypes', () => { afterAll(() => teardown()); + // Simulates return_public_parameters with min and max values for bool, Field, u64, i64, and a nested + // struct. Asserts that round-tripped values match the TS originals at both extremes. it('passes public parameters', async () => { const { result: minResult } = await abiTypesContract.methods .return_public_parameters(false, 0n, 0n, I64_MIN, { w: 0n, x: false, y: 0n, z: I64_MIN }) @@ -62,6 +65,7 @@ describe('AbiTypes', () => { ]); }); + // Same as public parameters but via a private function (return_private_parameters). it('passes private parameters', async () => { const { result: minResult } = await abiTypesContract.methods .return_private_parameters(false, 0n, 0n, I64_MIN, { w: 0n, x: false, y: 0n, z: I64_MIN }) @@ -87,6 +91,8 @@ describe('AbiTypes', () => { ]); }); + // Passes an EthAddress to the contract and asserts the return value is decoded as an EthAddress instance + // with the same value. it('decodes EthAddress return value', async () => { const ethAddr = EthAddress.fromString('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); @@ -98,6 +104,8 @@ describe('AbiTypes', () => { expect(result).toEqual(ethAddr); }); + // Passes a FunctionSelector to the contract and asserts round-trip decoding produces an equal + // FunctionSelector instance. it('decodes FunctionSelector return value', async () => { const selector = FunctionSelector.fromField(new Fr(0xdeadbeefn)); @@ -109,6 +117,7 @@ describe('AbiTypes', () => { expect(result).toEqual(selector); }); + // Passes a wrapped-field value and asserts the return is decoded as an Fr instance equal to Fr(42). it('decodes wrapped field struct as Fr', async () => { const value = new Fr(42n); @@ -120,6 +129,7 @@ describe('AbiTypes', () => { expect(result).toEqual(value); }); + // Same as public/private parameters but via a utility (unconstrained view) function. it('passes utility parameters', async () => { const { result: minResult } = await abiTypesContract.methods .return_utility_parameters(false, 0n, 0n, I64_MIN, { w: 0n, x: false, y: 0n, z: I64_MIN }) diff --git a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts index 4635f190c272..a082d3dfb8da 100644 --- a/yarn-project/end-to-end/src/e2e_account_contracts.test.ts +++ b/yarn-project/end-to-end/src/e2e_account_contracts.test.ts @@ -40,6 +40,9 @@ export class TestWalletInternals extends TestWallet { const itShouldBehaveLikeAnAccountContract = ( getAccountContract: (encryptionKey: GrumpkinScalar) => AccountContract, ) => { + // Shared suite parametrized over account contract type. Creates one account from the supplied + // AccountContract implementation (deploying it only if it has an initializer — initializerless + // variants skip the deploy tx) and exercises private calls, public calls, and signature failure. describe(`behaves like an account contract`, () => { let aztecNode: AztecNode; let logger: Logger; @@ -80,11 +83,13 @@ const itShouldBehaveLikeAnAccountContract = ( afterAll(() => teardown()); + // Sends a private function call on ChildContract and asserts it does not revert. it('calls a private function', async () => { logger.info('Calling private function...'); await child.methods.value(42).send({ from: completeAddress.address }); }); + // Calls pub_inc_value on the deployed Child contract and reads the resulting stored value via the node. it('calls a public function', async () => { logger.info('Calling public function...'); await child.methods.pub_inc_value(42).send({ from: completeAddress.address }); @@ -92,6 +97,8 @@ const itShouldBehaveLikeAnAccountContract = ( expect(storedValue).toEqual(new Fr(42n)); }); + // Swaps out the account's AuthWitnessProvider for one holding a random key, then simulates + // a private call and expects a "Cannot satisfy constraint" rejection. it('fails to call a function using an invalid signature', async () => { const randomContract = getAccountContract(GrumpkinScalar.random()); const authWitnessProvider = randomContract.getAuthWitnessProvider(completeAddress); @@ -108,6 +115,11 @@ const itShouldBehaveLikeAnAccountContract = ( }); }; +// Tests that multiple account contract implementations (Schnorr, Schnorr-initializerless, and ECDSA +// stored-key) satisfy the common account contract interface. Each variant gets its own +// setup(0, AUTOMINE_E2E_OPTS) with an additionallyFundedAccounts override, one node, automine sequencer, +// no extra nodes. (v5: added the initializerless variant and renamed initialFundedAccounts → +// additionallyFundedAccounts.) describe('e2e_account_contracts', () => { describe('schnorr account', () => { itShouldBehaveLikeAnAccountContract(() => new SchnorrAccountContract(GrumpkinScalar.random())); diff --git a/yarn-project/end-to-end/src/e2e_amm.test.ts b/yarn-project/end-to-end/src/e2e_amm.test.ts index 2fb58fad62ae..cec42bc3ac71 100644 --- a/yarn-project/end-to-end/src/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/e2e_amm.test.ts @@ -13,6 +13,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 900_000; +// End-to-end test for the AMM contract: liquidity provisioning, swaps, and removal. +// Uses setup(4, AUTOMINE_E2E_OPTS, {syncChainTip:'checkpointed'}) providing one node with automine +// sequencer, four funded accounts (admin, two LPs, swapper), and three deployed Token contracts. // TODO(F-560): Consider whether it makes sense to drop this // https://linear.app/aztec-labs/issue/F-560/add-more-tests-to-forward-compatibility-testing describe('AMM', () => { @@ -78,6 +81,9 @@ describe('AMM', () => { afterAll(() => teardown()); + // Happy-path integration covering all AMM operations in sequence: initial liquidity, + // second LP entering, exact-in swap, exact-out swap, and LP withdrawal. Tests are ordered + // and share state; they must run sequentially in this describe block. describe('full flow', () => { // This is an integration test in which we perform an entire run of the happy path. Thorough unit testing is not // included. @@ -106,6 +112,8 @@ describe('AMM', () => { expect(after.token1 - before.token1).toEqual(delta.token1); } + // First LP deposits maximum token0 and token1, receiving (99/100)*TOTAL_SUPPLY liquidity tokens. + // Verifies AMM and LP balances shift by the full deposited amounts. it('add initial liquidity', async () => { const ammBalancesBefore = await getAmmBalances(); const lpBalancesBefore = await getWalletBalances(liquidityProviderAddress); @@ -165,6 +173,8 @@ describe('AMM', () => { ); }); + // Second LP enters with a mismatched max ratio (6:5). The AMM uses the 1:1 pool ratio, + // refunds excess token0, and mints proportional liquidity tokens. it('add liquidity from another lp', async () => { // This is the same as when we add liquidity for the first time, but we'll be going through a different code path // since total supply for the liquidity token is non-zero @@ -237,6 +247,8 @@ describe('AMM', () => { ).toEqual(expectedLiquidityTokens); }); + // Swapper sends 10% of token0 balance as exact-in and receives the contract-quoted amount of + // token1 as exact-out minimum. Verifies swapper and AMM balance deltas match the quote. it('swap exact tokens in', async () => { const swapperBalancesBefore = await getWalletBalances(swapperAddress); const ammBalancesBefore = await getAmmBalances(); @@ -271,6 +283,9 @@ describe('AMM', () => { assertBalancesDelta(swapperBalancesBefore, swapperBalancesAfter, { token0: -amountIn, token1: amountOutMin }); }); + // Undoes the previous swap: requests exact-out equal to the token0 the contract would return + // for the swapper's full token1 balance. Verifies the swapper ends with less token0 than initially + // (fees consumed), and confirms balance deltas match the quote. it('swap exact tokens out', async () => { const swapperBalancesBefore = await getWalletBalances(swapperAddress); const ammBalancesBefore = await getAmmBalances(); @@ -315,6 +330,8 @@ describe('AMM', () => { expect(swapperBalancesAfter.token0).toBeLessThan(INITIAL_TOKEN_BALANCE); }); + // Second LP burns their entire liquidity-token balance via the AMM. Verifies they receive + // more token0 than their original deposit (swap fees accrued) and exactly their token1 back. it('remove liquidity', async () => { // We now withdraw all of the tokens of one of the liquidity providers by burning their entire liquidity token // balance. diff --git a/yarn-project/end-to-end/src/e2e_authwit.test.ts b/yarn-project/end-to-end/src/e2e_authwit.test.ts index 546cd7fa60d6..fadf558e9b19 100644 --- a/yarn-project/end-to-end/src/e2e_authwit.test.ts +++ b/yarn-project/end-to-end/src/e2e_authwit.test.ts @@ -15,6 +15,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 300_000; +// Tests the authorization witness (authwit) system in both private and public contexts. +// Uses setup(2, AUTOMINE_E2E_OPTS) providing one node with automine sequencer and two accounts. +// Accounts are publicly deployed and the AuthRegistry is published before any test runs. describe('e2e_authwit_tests', () => { jest.setTimeout(TIMEOUT); @@ -40,8 +43,12 @@ describe('e2e_authwit_tests', () => { afterAll(() => teardown()); + // Private authwit tests: witnesses are provided only to PXE, not published on-chain. describe('Private', () => { + // Tests inner-hash consumption via the AuthWitTest proxy flow. describe('arbitrary data', () => { + // Creates an inner hash, generates a private witness, asserts it is valid only for account1, + // consumes it via the proxy (making the inner hash a nullifier), then asserts double-spend is rejected. it('happy path', async () => { // What are we doing here: // 1. We compute an inner hash which is here just a hash of random data @@ -89,8 +96,13 @@ describe('e2e_authwit_tests', () => { }); }); + // Public authwit tests: witnesses are stored on-chain via setPublicAuthWit and consumed through + // the AuthRegistry contract. describe('Public', () => { + // Tests that a public authwit can be set, validated, consumed, and then appears invalid. describe('arbitrary data', () => { + // Sets a public authwit for account1, validates it is both private and public valid, + // then consumes it via the AuthRegistry and verifies it is no longer publicly valid. it('happy path', async () => { const innerHash = await computeInnerAuthWitHash([Fr.fromHexString('0xdead'), Fr.fromHexString('0x01')]); @@ -119,7 +131,10 @@ describe('e2e_authwit_tests', () => { }); }); + // Tests that a public authwit can be cancelled (set to false) before consumption. describe('failure case', () => { + // Sets a public authwit, then immediately revokes it, then attempts to consume — expects + // an "unauthorized" revert. it('cancel before usage', async () => { const innerHash = await computeInnerAuthWitHash([Fr.fromHexString('0xdead'), Fr.fromHexString('0x02')]); const intent = { consumer: auth.address, innerHash }; diff --git a/yarn-project/end-to-end/src/e2e_automine_smoke.test.ts b/yarn-project/end-to-end/src/e2e_automine_smoke.test.ts index 51548737aa57..f85a6c7f666c 100644 --- a/yarn-project/end-to-end/src/e2e_automine_smoke.test.ts +++ b/yarn-project/end-to-end/src/e2e_automine_smoke.test.ts @@ -14,6 +14,9 @@ import 'jest-extended'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Smoke tests for the AutomineSequencer: verifies that sequential and parallel txs land correctly, +// time warps work, mineBlock produces checkpoints, and revertToCheckpoint restores chain state. +// Uses setup(1, AUTOMINE_E2E_OPTS) providing one node with AutomineSequencer. describe('e2e_automine_smoke', () => { jest.setTimeout(10 * 60 * 1000); @@ -40,6 +43,8 @@ describe('e2e_automine_smoke', () => { afterAll(() => teardown()); + // Sends 5 txs sequentially and asserts each lands in its own consecutive block, + // confirming the automine sequencer serializes dependent txs correctly. it('mines sequential dependent txs back-to-back', async () => { const startBlock = await aztecNode.getBlockNumber(); const blockNumbers: number[] = []; @@ -53,6 +58,8 @@ describe('e2e_automine_smoke', () => { expect(blockNumbers).toEqual(range(5, startBlock + 1)); }); + // Sends 5 txs in parallel (Promise.all) and asserts all receipts have a block number + // greater than the starting block. it('parallel sends all land', async () => { const startBlock = await aztecNode.getBlockNumber(); @@ -65,6 +72,8 @@ describe('e2e_automine_smoke', () => { } }); + // Calls warpL2TimeAtLeastBy(24s), then asserts the L1 timestamp advanced by at least 24s and + // the next sent tx lands in a valid block. it('warp advances L1 timestamp and the next tx lands at a fresh slot', async () => { const before = await cheatCodes.eth.lastBlockTimestamp(); const warpBy = 24; @@ -97,6 +106,7 @@ describe('e2e_automine_smoke', () => { expect(await aztecNode.prove(CheckpointNumber(checkpointed + 100))).toBe(checkpointed); }); + // Calls aztecNode.mineBlock() and asserts both checkpointed block and checkpoint numbers advance. it('mineBlock produces an empty checkpoint', async () => { const before = await aztecNode.getChainTips(); await aztecNode.mineBlock(); @@ -105,6 +115,8 @@ describe('e2e_automine_smoke', () => { expect(after.checkpointed.block.number).toBeGreaterThan(before.checkpointed.block.number); }); + // Mines two checkpoints, then calls automine.revertToCheckpoint(first). Asserts the archiver tip + // reverts to the earlier checkpoint and a fresh tx lands cleanly on the reverted chain. it('revertToCheckpoint rolls back L1+L2 state', async () => { // Land a tx and record the checkpoint it landed at. await contract.methods.emit_nullifier_public(BigInt(5000)).send({ from: owner }); @@ -128,6 +140,8 @@ describe('e2e_automine_smoke', () => { expect(r3.blockNumber).toBeGreaterThan(0); }); + // Fires 6 tx sends without awaiting, inserting warpL2TimeAtLeastBy(24) between every two sends. + // Awaits all via Promise.all and verifies all 6 receipts have valid block numbers. it('interleaved txs and warps all land successfully', async () => { const startBlock = await aztecNode.getBlockNumber(); const startL1Ts = await cheatCodes.eth.lastBlockTimestamp(); diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index 554e336ea56a..8da1fbee15a0 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -14,6 +14,10 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 600_000; +// End-to-end tests for AVM (Aztec Virtual Machine) execution: assertions, storage, nullifiers, +// nested calls, gas metering, contract instances, L2→L1 messages, and public-storage overrides. +// Uses setup(1, AUTOMINE_E2E_OPTS) providing one node, automine sequencer, one funded account. +// CI runs this as a separate job with TIMEOUT=30m and optionally dumps AVM circuit inputs. describe('e2e_avm_simulator', () => { jest.setTimeout(TIMEOUT); @@ -33,9 +37,11 @@ describe('e2e_avm_simulator', () => { afterAll(() => teardown()); + // Tests for the AvmTestContract, grouped by whether they need a fresh contract per test. describe('AvmTestContract', () => { // Read-only / non-mutating tests share a single deployment to keep slot-paced deploy txs // out of the per-test critical path under proposer pipelining. + // Shared single AvmTestContract instance for all non-mutating tests. describe('with shared deployment', () => { let avmContract: AvmTestContract; let avmContractInstance: ContractInstanceWithAddress; @@ -46,6 +52,7 @@ describe('e2e_avm_simulator', () => { })); }); + // Tests that assertion failures and intrinsic errors produce enriched messages with source locations. describe('Assertions & error enriching', () => { /** * Expect an error like: @@ -58,7 +65,10 @@ describe('e2e_avm_simulator', () => { * let call = quote { $name($args) (/home/aztec-dev/aztec-packages/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr:59:20) * at AvmTest.0xc3515746 */ + // Direct (non-nested) assertion failures on AvmTestContract. describe('Not nested', () => { + // Simulates assertion_failure() and expects an error message matching the Noir assert string + // and a stack trace that includes the inner helper and the AVM dispatcher. it('PXE processes user code assertions and recovers message (properly enriched)', async () => { await expect( avmContract.methods.assertion_failure().simulate({ from: defaultAccountAddress }), @@ -71,23 +81,30 @@ describe('e2e_avm_simulator', () => { }), ); }); + // Simulates assert_nullifier_exists with a non-existent nullifier and expects + // "Nullifier doesn't exist!" in the error message. it('PXE processes user code assertions and recovers message (complex)', async () => { await expect( avmContract.methods.assert_nullifier_exists(123).simulate({ from: defaultAccountAddress }), ).rejects.toThrow("Assertion failed: Nullifier doesn't exist!"); }); + // Simulates divide_by_zero(0) and expects "Division by zero" in the error message. it('PXE processes intrinsic assertions and recovers message', async () => { await expect( avmContract.methods.divide_by_zero(0).simulate({ from: defaultAccountAddress }), ).rejects.toThrow('Division by zero'); }); }); + // Assertion failures propagated through external (nested) contract calls. describe('Nested', () => { + // Calls external_call_to_assertion_failure which nests into another function that asserts false. it('PXE processes user code assertions and recovers message', async () => { await expect( avmContract.methods.external_call_to_assertion_failure().simulate({ from: defaultAccountAddress }), ).rejects.toThrow('Assertion failed: This assertion should fail!'); }); + // Calls external_call_to_divide_by_zero which nests into a divide-by-zero and expects + // "Division by zero" propagated through the nested call frame. it('PXE processes intrinsic assertions and recovers message', async () => { await expect( avmContract.methods.external_call_to_divide_by_zero().simulate({ from: defaultAccountAddress }), @@ -96,7 +113,9 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests that a private function can enqueue a public AVM function. describe('From private', () => { + // Simulates a tx that enqueues enqueue_public_from_private and asserts no revert reason. it('Should enqueue a public function correctly', async () => { const request = await avmContract.methods.enqueue_public_from_private().request(); const simulation = await wallet.simulateTx(request, { from: defaultAccountAddress }); @@ -104,7 +123,10 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests that gas used by AVM execution is reported in simulation results. describe('Gas metering', () => { + // Simulates add_args_return and checks that teardown-adjusted L2 gas is in a reasonable range + // and not a suspiciously round number. it('Tracks L2 gas usage on simulation', async () => { const request = await avmContract.methods.add_args_return(20n, 30n).request(); const simulation = await wallet.simulateTx(request, { from: defaultAccountAddress }); @@ -119,7 +141,10 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests AVM's ability to introspect its own contract instance fields. describe('Contract instance', () => { + // Calls test_get_contract_instance_matches with the known deployer, class id, and hashes, + // and asserts the tx succeeds. it('Works', async () => { const { receipt: tx } = await avmContract.methods .test_get_contract_instance_matches( @@ -134,7 +159,10 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests that L2→L1 message emission validates the recipient ethereum address. describe('L2 to L1 messages', () => { + // Sends raw_l2_to_l1_msg with Fr.MAX_FIELD_VALUE as recipient (not a valid Eth address) + // and expects a revert. it('Should fail if emitting to an invalid ethereum address', async () => { const recipient = Fr.MAX_FIELD_VALUE; await expect( @@ -145,7 +173,11 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests behavior of AVM nested calls: non-existent contracts, error recovery, and + // duplicate nullifier enforcement across contract boundaries. describe('Nested calls', () => { + // Calls nested_call_to_nothing which calls a non-deployed contract. Expects a "not deployed" + // error to propagate through the default rethrow policy. it('Nested call to non-existent contract reverts & rethrows by default', async () => { // The nested call reverts and by default caller rethrows await expect( @@ -153,6 +185,8 @@ describe('e2e_avm_simulator', () => { ).rejects.toThrow(/not deployed/); }); + // Calls nested_call_to_nothing_recovers which catches the nested failure and continues. + // Asserts the tx execution result is SUCCESS. it('Nested CALL instruction to non-existent contract returns failure, but caller can recover', async () => { // The nested call reverts (returns failure), but the caller doesn't HAVE to rethrow. const { receipt: tx } = await avmContract.methods @@ -160,6 +194,8 @@ describe('e2e_avm_simulator', () => { .send({ from: defaultAccountAddress }); expect(tx.executionResult).toEqual(TxExecutionResult.SUCCESS); }); + // Calls create_same_nullifier_in_nested_call with the same contract address, expects a revert + // because both calls emit the same unsiloed nullifier under the same contract silo. it('Should NOT be able to emit the same unsiloed nullifier from the same contract', async () => { const nullifier = new Fr(1); await expect( @@ -173,6 +209,7 @@ describe('e2e_avm_simulator', () => { // State-mutating tests get a fresh deployment per test to avoid cross-test leakage of // storage writes or persisted nullifiers. + // Each test deploys one or two fresh AvmTestContract instances in beforeEach. describe('with fresh deployment per test', () => { let avmContract: AvmTestContract; let secondAvmContract: AvmTestContract; @@ -184,7 +221,9 @@ describe('e2e_avm_simulator', () => { ({ contract: secondAvmContract } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress })); }); + // Tests AVM read/write to public storage slots (Field and Map). describe('Storage', () => { + // Calls set_storage_single(20), then simulates read_storage_single and expects 20. it('Modifies storage (Field)', async () => { await avmContract.methods.set_storage_single(20n).send({ from: defaultAccountAddress }); expect( @@ -192,6 +231,7 @@ describe('e2e_avm_simulator', () => { ).toEqual(20n); }); + // Calls set_storage_map then add_storage_map, reads the result and expects 200. it('Modifies storage (Map)', async () => { const address = AztecAddress.fromBigInt(9090n); await avmContract.methods.set_storage_map(address, 100).send({ from: defaultAccountAddress }); @@ -201,6 +241,8 @@ describe('e2e_avm_simulator', () => { ).toEqual(200n); }); + // Uses BatchCall to enqueue set_storage_map + add_storage_map in a single tx, then reads; + // confirms storage is shared across enqueued public calls within the same tx. it('Preserves storage across enqueued public calls', async () => { const address = AztecAddress.fromBigInt(9090n); // This will create 1 tx with 2 public calls in it. @@ -215,8 +257,10 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests nullifier emission and existence checking in various tx/call orderings. describe('Nullifiers', () => { // Nullifier will not yet be siloed by the kernel. + // Emits a nullifier and checks its existence within the same tx. it('Emit and check in the same tx', async () => { const { receipt: tx } = await avmContract.methods .emit_nullifier_and_check(123456) @@ -225,6 +269,7 @@ describe('e2e_avm_simulator', () => { }); // Nullifier will have been siloed by the kernel, but we check against the unsiloed one. + // Emits a nullifier in one tx, then in a second tx asserts_nullifier_exists using the unsiloed value. it('Emit and check in separate tx', async () => { const nullifier = new Fr(123456); let { receipt: tx } = await avmContract.methods @@ -238,6 +283,8 @@ describe('e2e_avm_simulator', () => { expect(tx.executionResult).toEqual(TxExecutionResult.SUCCESS); }); + // Uses BatchCall to emit in one enqueued call then assert in a second enqueued call, + // within a single tx. it('Emit and check in separate enqueued calls but same tx', async () => { const nullifier = new Fr(123456); @@ -249,7 +296,9 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests nullifier emission across same-contract and cross-contract nested call boundaries. describe('Nested calls', () => { + // Emits two different unsiloed nullifiers from the same contract via nested call. it('Should be able to emit different unsiloed nullifiers from the same contract', async () => { const nullifier = new Fr(1); const { receipt: tx } = await avmContract.methods @@ -258,6 +307,7 @@ describe('e2e_avm_simulator', () => { expect(tx.executionResult).toEqual(TxExecutionResult.SUCCESS); }); + // Two different contracts emit the same unsiloed nullifier; different silos mean no collision. it('Should be able to emit the same unsiloed nullifier from two different contracts', async () => { const nullifier = new Fr(1); const { receipt: tx } = await avmContract.methods @@ -266,6 +316,7 @@ describe('e2e_avm_simulator', () => { expect(tx.executionResult).toEqual(TxExecutionResult.SUCCESS); }); + // Two different contracts emit two different unsiloed nullifiers; no collision expected. it('Should be able to emit different unsiloed nullifiers from two different contracts', async () => { const nullifier = new Fr(1); const { receipt: tx } = await avmContract.methods @@ -277,6 +328,7 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests that simulation-level publicStorage overrides shadow real storage without mutating the chain. describe('publicDataOverrides', () => { // AvmTestContract: `single` is the first storage variable and lives at raw slot 1. const SINGLE_SLOT = new Fr(1n); @@ -286,6 +338,8 @@ describe('e2e_avm_simulator', () => { ({ contract: avmContract } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress })); }); + // Supplies an override for a never-written slot, simulates a read, and confirms the override value + // is returned while the on-chain slot remains zero. it('simulated read of an unwritten slot returns the override; real storage is untouched', async () => { const overrideValue = new Fr(0xdeadbeefn); const publicStorage: PublicStorageOverride[] = [ @@ -302,6 +356,8 @@ describe('e2e_avm_simulator', () => { expect(realValue.toBigInt()).toEqual(0n); }); + // Writes a real value via a tx, then simulates with a different override and confirms the override + // wins in simulation while the real stored value is unchanged. it('simulated read returns the override when a slot was previously written by a real tx', async () => { const realValue = new Fr(100n); await avmContract.methods.set_storage_single(realValue).send({ from: defaultAccountAddress }); @@ -322,6 +378,7 @@ describe('e2e_avm_simulator', () => { }); }); + // Tests that the AvmInitializerTestContract correctly initializes immutable storage at deployment. describe('AvmInitializerTestContract', () => { let avmContract: AvmInitializerTestContract; @@ -331,7 +388,9 @@ describe('e2e_avm_simulator', () => { })); }); + // Tests that immutable storage set in the constructor is readable after deployment. describe('Storage', () => { + // Simulates read_storage_immutable and expects the constructor-set value of 42. it('Read immutable (initialized) storage (Field)', async () => { expect( (await avmContract.methods.read_storage_immutable().simulate({ from: defaultAccountAddress })).result, diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts index 15c51b48c84f..8b2b11243474 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts @@ -3,6 +3,10 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { BlacklistTokenContractTest, Role } from './blacklist_token_contract_test.js'; +// Covers role management (admin grant/revoke, minter assignment, blacklisting) on the TokenBlacklist contract. +// Setup: single node with AutomineSequencer (AUTOMINE_E2E_OPTS), 3 deployed accounts (admin/other/blacklisted), +// TokenBlacklist contract deployed. Role changes require crossing a 86400s L2 time delay enforced by the +// contract; crossTimestampOfChange() handles this via markAsProven + warpL2TimeAtLeastBy. describe('e2e_blacklist_token_contract access control', () => { const t = new BlacklistTokenContractTest('access_control'); @@ -18,6 +22,8 @@ describe('e2e_blacklist_token_contract access control', () => { await t.tokenSim.check(); }); + // Sends update_roles to grant admin+minter to the admin account, crosses the 86400s delay, then asserts + // the role is readable via get_roles. it('grant mint permission to the admin', async () => { const adminMinterRole = new Role().withAdmin().withMinter(); await t.asset.methods.update_roles(t.adminAddress, adminMinterRole.toNoirStruct()).send({ from: t.adminAddress }); @@ -29,6 +35,7 @@ describe('e2e_blacklist_token_contract access control', () => { ); }); + // Grants admin role to the 'other' account, crosses the delay, and verifies the role via get_roles. it('create a new admin', async () => { const adminRole = new Role().withAdmin(); await t.asset.methods.update_roles(t.otherAddress, adminRole.toNoirStruct()).send({ from: t.adminAddress }); @@ -40,6 +47,7 @@ describe('e2e_blacklist_token_contract access control', () => { ); }); + // Clears the 'other' account's roles via update_roles, crosses the delay, and verifies the empty role. it('revoke the new admin', async () => { const noRole = new Role(); await t.asset.methods.update_roles(t.otherAddress, noRole.toNoirStruct()).send({ from: t.adminAddress }); @@ -51,6 +59,7 @@ describe('e2e_blacklist_token_contract access control', () => { ); }); + // Assigns blacklisted role to the dedicated blacklistedAddress, crosses the delay, and reads back the role. it('blacklist account', async () => { const blacklistRole = new Role().withBlacklisted(); await t.asset.methods @@ -64,7 +73,9 @@ describe('e2e_blacklist_token_contract access control', () => { ); }); + // Verifies that update_roles reverts when called by a non-admin account. describe('failure cases', () => { + // Calls update_roles from otherAddress (not admin) and expects the 'caller is not admin' assertion failure. it('set roles from non admin', async () => { const newRole = new Role().withAdmin().withAdmin(); await expect( @@ -74,6 +85,7 @@ describe('e2e_blacklist_token_contract access control', () => { ).rejects.toThrow('Assertion failed: caller is not admin'); }); + // Attempts to revoke admin's minter role from otherAddress and expects the 'caller is not admin' error. it('revoke minter from non admin', async () => { const noRole = new Role(); await expect( diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts index 3b7e65effb29..aac62974e3a9 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts @@ -6,6 +6,10 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { DUPLICATE_NULLIFIER_ERROR, U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers public and private burn operations on TokenBlacklist, including authwit-delegated burns and +// blacklist enforcement. Setup: single node with AutomineSequencer, 3 accounts, TokenBlacklist deployed, +// initial mint applied (admin has both public and private balances). Time-warp required to cross +// role-change delay (86400s) during setup. describe('e2e_blacklist_token_contract burn', () => { const t = new BlacklistTokenContractTest('burn'); let { asset, tokenSim, wallet, adminAddress, otherAddress, blacklistedAddress } = t; @@ -26,7 +30,9 @@ describe('e2e_blacklist_token_contract burn', () => { await t.tokenSim.check(); }); + // Public burn path: direct burns and authwit-delegated burns. describe('public', () => { + // Burns half the admin's public balance and verifies via TokenSimulator. it('burn less than balance', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -39,6 +45,8 @@ describe('e2e_blacklist_token_contract burn', () => { tokenSim.burnPublic(adminAddress, amount); }); + // Grants a public authwit for burn, burns via otherAddress, then asserts the authwit is consumed + // (replay reverts with unauthorized). it('burn on behalf of other', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -66,7 +74,9 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(/unauthorized/); }); + // Error paths for public burn: overflow, nonce, missing approval, wrong caller, blacklist. describe('failure cases', () => { + // Attempts to burn more than the current balance and expects U128_UNDERFLOW_ERROR. it('burn more than balance', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -79,6 +89,7 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Verifies that self-burn with a non-zero nonce reverts with the invalid-nonce assertion. it('burn on behalf of self with non-zero nonce', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -94,6 +105,7 @@ describe('e2e_blacklist_token_contract burn', () => { ); }); + // Calls burn_public on behalf of admin from otherAddress without any authwit and expects unauthorized. it('burn on behalf of other without "approval"', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -106,6 +118,7 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(/unauthorized/); }); + // Approves a burn of more than balance via authwit, then expects U128_UNDERFLOW_ERROR on simulate. it('burn more than balance on behalf of other', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -127,6 +140,8 @@ describe('e2e_blacklist_token_contract burn', () => { await expect(action.simulate({ from: otherAddress })).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Creates an authwit designating adminAddress as the caller but executes from otherAddress; expects + // unauthorized because the caller doesn't match the authwit. it('burn on behalf of other, wrong designated caller', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -150,6 +165,7 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(/unauthorized/); }); + // Verifies that a blacklisted account cannot burn its own tokens (Blacklisted: Sender). it('burn from blacklisted account', async () => { await expect( asset.methods.burn_public(blacklistedAddress, 1n, 0).simulate({ from: blacklistedAddress }), @@ -158,7 +174,9 @@ describe('e2e_blacklist_token_contract burn', () => { }); }); + // Private burn path: direct burns and authwit-delegated burns via proxy. describe('private', () => { + // Burns half the admin's private balance and verifies via TokenSimulator. it('burn less than balance', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -170,6 +188,8 @@ describe('e2e_blacklist_token_contract burn', () => { tokenSim.burnPrivate(adminAddress, amount); }); + // Creates a private authwit for burn, sends it through the proxy (so msg_sender differs from note owner), + // verifies TokenSimulator, then asserts replay reverts with DUPLICATE_NULLIFIER_ERROR. it('burn on behalf of other', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -192,7 +212,9 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths for private burn: overflow, nonce, missing approval, wrong caller, blacklist. describe('failure cases', () => { + // Attempts to burn more than private balance and expects the 'Balance too low' assertion. it('burn more than balance', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -205,6 +227,7 @@ describe('e2e_blacklist_token_contract burn', () => { ); }); + // Verifies that self-burn with nonce=1 reverts with the invalid-nonce assertion. it('burn on behalf of self with non-zero nonce', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -217,6 +240,7 @@ describe('e2e_blacklist_token_contract burn', () => { ); }); + // Creates authwit for a burn exceeding balance; expects 'Balance too low' when simulated through proxy. it('burn more than balance on behalf of other', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -235,6 +259,7 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Simulates burn through proxy without providing a witness; expects unknown-authwit error. it('burn on behalf of other without approval', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -257,6 +282,8 @@ describe('e2e_blacklist_token_contract burn', () => { ); }); + // Creates authwit designating otherAddress as caller but sends through proxy; expects unknown-authwit error + // because the computed message hash doesn't match the proxy's address. it('on behalf of other (invalid designated caller)', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -281,6 +308,7 @@ describe('e2e_blacklist_token_contract burn', () => { ).rejects.toThrow(`Unknown auth witness for message hash ${expectedMessageHash.toString()}`); }); + // Verifies that a blacklisted account cannot private-burn its tokens (Blacklisted: Sender). it('burn from blacklisted account', async () => { await expect( asset.methods.burn(blacklistedAddress, 1n, 0).simulate({ from: blacklistedAddress }), diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts index 95949cf2138f..ab1ed42868c6 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts @@ -6,6 +6,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { U128_OVERFLOW_ERROR } from '../fixtures/index.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers public and private minting on TokenBlacklist, including minter role enforcement and blacklist +// restrictions on recipients. Setup: single node with AutomineSequencer, 3 accounts, TokenBlacklist +// deployed with initial balances (applyMint). Role-change delay requires time-warp during setup. describe('e2e_blacklist_token_contract mint', () => { const t = new BlacklistTokenContractTest('mint'); let { asset, tokenSim, adminAddress, otherAddress, blacklistedAddress } = t; @@ -30,14 +33,18 @@ describe('e2e_blacklist_token_contract mint', () => { await t.tokenSim.check(); }); + // Public mint path: success and failure cases including overflow and blacklist enforcement. describe('Public', () => { + // Mints 10000 tokens publicly as the admin-minter and verifies balance via TokenSimulator. it('as minter', async () => { const amount = 10000n; tokenSim.mintPublic(adminAddress, amount); await asset.methods.mint_public(adminAddress, amount).send({ from: adminAddress }); }); + // Error paths: non-minter, overflow (recipient balance), overflow (total supply), blacklisted recipient. describe('failure cases', () => { + // Attempts mint_public from otherAddress (not a minter) and expects 'caller is not minter'. it('as non-minter', async () => { const amount = 10000n; await expect(asset.methods.mint_public(adminAddress, amount).simulate({ from: otherAddress })).rejects.toThrow( @@ -45,6 +52,7 @@ describe('e2e_blacklist_token_contract mint', () => { ); }); + // Mints an amount that would overflow the recipient's u128 balance; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPublic(adminAddress); await expect(asset.methods.mint_public(adminAddress, amount).simulate({ from: adminAddress })).rejects.toThrow( @@ -52,6 +60,7 @@ describe('e2e_blacklist_token_contract mint', () => { ); }); + // Mints an amount that would overflow total supply across different recipients; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPublic(adminAddress); await expect(asset.methods.mint_public(otherAddress, amount).simulate({ from: adminAddress })).rejects.toThrow( @@ -59,6 +68,7 @@ describe('e2e_blacklist_token_contract mint', () => { ); }); + // Tries to mint to the blacklisted account and expects the 'Blacklisted: Recipient' assertion. it('mint to blacklisted entity', async () => { await expect( asset.methods.mint_public(blacklistedAddress, 1n).simulate({ from: adminAddress }), @@ -67,6 +77,7 @@ describe('e2e_blacklist_token_contract mint', () => { }); }); + // Private mint path: mint_private + redeem_shield flow, plus failure cases. describe('Private', () => { const secret = Fr.random(); const amount = 10000n; @@ -77,7 +88,9 @@ describe('e2e_blacklist_token_contract mint', () => { secretHash = await computeSecretHash(secret); }); + // Happy path for private minting: mint, register the pending shield note in PXE, and redeem. describe('Mint flow', () => { + // Mints privately as admin-minter, adds the pending shield note to PXE, redeems it, and checks balance. it('mint_private as minter and redeem as recipient', async () => { const { result: balanceBefore } = await asset.methods .balance_of_private(adminAddress) @@ -98,7 +111,9 @@ describe('e2e_blacklist_token_contract mint', () => { }); }); + // Error paths for private minting: double-spend, non-minter, overflow, blacklist on redeem. describe('failure cases', () => { + // Adds the already-redeemed shield note to a second account's PXE and expects 'note not popped' on simulate. it('try to redeem as recipient again (double-spend) [REVERTS]', async () => { // We have another wallet add the note to their PXE and then try to spend it. They will be able to successfully // add it, but PXE will realize that the note has been nullified already and not inject it into the circuit @@ -111,12 +126,14 @@ describe('e2e_blacklist_token_contract mint', () => { ).rejects.toThrow(`Assertion failed: note not popped`); }); + // Attempts mint_private from otherAddress (not a minter) and expects 'caller is not minter'. it('mint_private as non-minter', async () => { await expect(asset.methods.mint_private(amount, secretHash).simulate({ from: otherAddress })).rejects.toThrow( 'Assertion failed: caller is not minter', ); }); + // Mints an amount that would overflow the recipient's private u128 balance; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPrivate(adminAddress); expect(amount).toBeLessThan(2n ** 128n); @@ -125,6 +142,7 @@ describe('e2e_blacklist_token_contract mint', () => { ); }); + // Mints an amount that would overflow total supply (private path); expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.totalSupply; await expect(asset.methods.mint_private(amount, secretHash).simulate({ from: adminAddress })).rejects.toThrow( @@ -132,6 +150,7 @@ describe('e2e_blacklist_token_contract mint', () => { ); }); + // Attempts redeem_shield targeting blacklistedAddress and expects 'Blacklisted: Recipient'. it('mint and try to redeem at blacklist', async () => { await expect( asset.methods.redeem_shield(blacklistedAddress, amount, secret).simulate({ from: adminAddress }), diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts index 92171c469a05..b4b3f84917e0 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts @@ -5,6 +5,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers the shield (public→private) and redeem_shield operations on TokenBlacklist, including +// authwit-delegated shielding and blacklist enforcement. Setup: single node with AutomineSequencer, +// 3 accounts, initial mint applied. Time-warp required during setup to cross role-change delay. describe('e2e_blacklist_token_contract shield + redeem_shield', () => { const t = new BlacklistTokenContractTest('shield'); let { asset, tokenSim, wallet, adminAddress, otherAddress, blacklistedAddress } = t; @@ -31,6 +34,8 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { secretHash = await computeSecretHash(secret); }); + // Shields half the admin's public balance to private, registers the note in PXE, redeems it, and + // verifies the result against TokenSimulator. it('on behalf of self', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -50,6 +55,8 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { await t.tokenSim.check(); }); + // Sets a public authwit allowing otherAddress to shield admin's tokens, executes the shield from + // otherAddress, verifies replay fails (unauthorized), redeems, and checks TokenSimulator. it('on behalf of other', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -84,7 +91,9 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { await t.tokenSim.check(); }); + // Error paths: more-than-balance, invalid nonce, wrong caller, missing approval, blacklist. describe('failure cases', () => { + // Shields more than public balance (self); expects U128_UNDERFLOW_ERROR. it('on behalf of self (more than balance)', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -98,6 +107,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { ).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Self-shield with nonce=1; expects invalid-nonce assertion failure. it('on behalf of self (invalid authwit nonce)', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -113,6 +123,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { ); }); + // Authwit-shields more than balance via otherAddress; expects U128_UNDERFLOW_ERROR. it('on behalf of other (more than balance)', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -134,6 +145,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { await expect(action.simulate({ from: otherAddress })).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Approves otherAddress as caller, executes from blacklistedAddress; expects unauthorized. it('on behalf of other (wrong designated caller)', async () => { const balancePub = await asset.methods .balance_of_public(adminAddress) @@ -155,6 +167,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { await expect(action.simulate({ from: blacklistedAddress })).rejects.toThrow(/unauthorized/); }); + // Calls shield for admin from otherAddress without any authwit; expects unauthorized. it('on behalf of other (without approval)', async () => { const balance = await asset.methods .balance_of_public(adminAddress) @@ -169,6 +182,7 @@ describe('e2e_blacklist_token_contract shield + redeem_shield', () => { ).rejects.toThrow(/unauthorized/); }); + // Attempts shield from the blacklisted account; expects 'Blacklisted: Sender' assertion. it('shielding from blacklisted account', async () => { await expect( asset.methods.shield(blacklistedAddress, 1n, secretHash, 0).simulate({ from: blacklistedAddress }), diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts index 18481c443e02..1231e8c2df2f 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts @@ -5,6 +5,9 @@ import { sendThroughAuthwitProxy, simulateThroughAuthwitProxy } from '../fixture import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from '../fixtures/fixtures.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers private token transfers on TokenBlacklist: direct, self-transfer, authwit-delegated, and +// blacklist enforcement. Setup: single node with AutomineSequencer, 3 accounts, initial mint applied. +// Time-warp required during setup to cross role-change delay. describe('e2e_blacklist_token_contract transfer private', () => { const t = new BlacklistTokenContractTest('transfer_private'); let { asset, tokenSim, wallet, adminAddress, otherAddress, blacklistedAddress } = t; @@ -25,6 +28,7 @@ describe('e2e_blacklist_token_contract transfer private', () => { await t.tokenSim.check(); }); + // Transfers half of admin's private balance to other and verifies via TokenSimulator. it('transfer less than balance', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -37,6 +41,7 @@ describe('e2e_blacklist_token_contract transfer private', () => { tokenSim.transferPrivate(adminAddress, otherAddress, amount); }); + // Transfers half of admin's private balance to themselves and verifies balance is unchanged. it('transfer to self', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -49,6 +54,8 @@ describe('e2e_blacklist_token_contract transfer private', () => { tokenSim.transferPrivate(adminAddress, adminAddress, amount); }); + // Creates a private authwit for transfer, sends through proxy, verifies TokenSimulator, then asserts + // replay fails with DUPLICATE_NULLIFIER_ERROR. it('transfer on behalf of other', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -71,7 +78,10 @@ describe('e2e_blacklist_token_contract transfer private', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths: over-balance, invalid nonce, over-balance via authwit, missing approval, wrong caller, + // sender blacklisted, recipient blacklisted. describe('failure cases', () => { + // Attempts to transfer more than private balance; expects 'Balance too low'. it('transfer more than balance', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -85,6 +95,7 @@ describe('e2e_blacklist_token_contract transfer private', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Self-transfer with nonce=1; expects invalid-nonce assertion. it('transfer on behalf of self with non-zero nonce', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -100,6 +111,7 @@ describe('e2e_blacklist_token_contract transfer private', () => { ); }); + // Authwit-transfers more than balance via proxy; expects 'Balance too low' and verifies balances unchanged. it('transfer more than balance on behalf of other', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -141,6 +153,7 @@ describe('e2e_blacklist_token_contract transfer private', () => { // See https://github.com/AztecProtocol/aztec-packages/issues/1259 }); + // Simulates transfer through proxy without providing a witness; expects unknown-authwit error. it('transfer on behalf of other without approval', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -163,6 +176,8 @@ describe('e2e_blacklist_token_contract transfer private', () => { ); }); + // Creates authwit designating otherAddress as caller but sends through proxy; expects unknown-authwit + // because the message hash references the proxy address, not otherAddress. it('transfer on behalf of other, wrong designated caller', async () => { const balance0 = await asset.methods .balance_of_private(adminAddress) @@ -193,12 +208,14 @@ describe('e2e_blacklist_token_contract transfer private', () => { ).toEqual(balance0); }); + // Attempts transfer from blacklistedAddress as sender; expects 'Blacklisted: Sender'. it('transfer from a blacklisted account', async () => { await expect( asset.methods.transfer(blacklistedAddress, adminAddress, 1n, 0).simulate({ from: blacklistedAddress }), ).rejects.toThrow('Assertion failed: Blacklisted: Sender'); }); + // Attempts transfer to blacklistedAddress as recipient; expects 'Blacklisted: Recipient'. it('transfer to a blacklisted account', async () => { await expect( asset.methods.transfer(adminAddress, blacklistedAddress, 1n, 0).simulate({ from: adminAddress }), diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts index 1efd41353ddd..65577ab71af5 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts @@ -4,6 +4,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers public token transfers on TokenBlacklist: direct, self, authwit-delegated, and blacklist +// enforcement. Setup: single node with AutomineSequencer, 3 accounts, initial mint applied. +// Time-warp required during setup to cross role-change delay. describe('e2e_blacklist_token_contract transfer public', () => { const t = new BlacklistTokenContractTest('transfer_public'); let { asset, tokenSim, wallet, adminAddress, otherAddress, blacklistedAddress } = t; @@ -24,6 +27,7 @@ describe('e2e_blacklist_token_contract transfer public', () => { await t.tokenSim.check(); }); + // Transfers half of admin's public balance to other and verifies via TokenSimulator. it('transfer less than balance', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -36,6 +40,7 @@ describe('e2e_blacklist_token_contract transfer public', () => { tokenSim.transferPublic(adminAddress, otherAddress, amount); }); + // Transfers half of admin's public balance to themselves; verifies balance unchanged via TokenSimulator. it('transfer to self', async () => { const balance = await asset.methods .balance_of_public(adminAddress) @@ -48,6 +53,8 @@ describe('e2e_blacklist_token_contract transfer public', () => { tokenSim.transferPublic(adminAddress, adminAddress, amount); }); + // Sets a public authwit allowing otherAddress to transfer admin's tokens, executes, verifies TokenSimulator, + // then confirms replay reverts with unauthorized. it('transfer on behalf of other', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -76,7 +83,10 @@ describe('e2e_blacklist_token_contract transfer public', () => { ).rejects.toThrow(/unauthorized/); }); + // Error paths: over-balance, invalid nonce, no approval, over-balance via authwit, wrong caller, + // sender blacklisted, recipient blacklisted. describe('failure cases', () => { + // Attempts to transfer more than public balance; expects U128_UNDERFLOW_ERROR. it('transfer more than balance', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -91,6 +101,7 @@ describe('e2e_blacklist_token_contract transfer public', () => { ).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Self-transfer with nonce=1; expects the invalid-nonce assertion failure. it('transfer on behalf of self with non-zero nonce', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -107,6 +118,7 @@ describe('e2e_blacklist_token_contract transfer public', () => { ); }); + // Calls transfer_public on behalf of admin without authwit; expects unauthorized. it('transfer on behalf of other without "approval"', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -121,6 +133,8 @@ describe('e2e_blacklist_token_contract transfer public', () => { ).rejects.toThrow(/unauthorized/); }); + // Approves a transfer exceeding balance via authwit; expects U128_UNDERFLOW_ERROR and verifies + // balances unchanged after simulate. it('transfer more than balance on behalf of other', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -160,6 +174,8 @@ describe('e2e_blacklist_token_contract transfer public', () => { ).toEqual(balance1); }); + // Approves adminAddress as the caller but executes from otherAddress; expects unauthorized and verifies + // balances unchanged. it('transfer on behalf of other, wrong designated caller', async () => { const balance0 = await asset.methods .balance_of_public(adminAddress) @@ -207,12 +223,14 @@ describe('e2e_blacklist_token_contract transfer public', () => { // See https://github.com/AztecProtocol/aztec-packages/issues/1259 }); + // Attempts transfer_public from the blacklisted account; expects 'Blacklisted: Sender'. it('transfer from a blacklisted account', async () => { await expect( asset.methods.transfer_public(blacklistedAddress, adminAddress, 1n, 0n).simulate({ from: blacklistedAddress }), ).rejects.toThrow('Assertion failed: Blacklisted: Sender'); }); + // Attempts transfer_public to the blacklisted account; expects 'Blacklisted: Recipient'. it('transfer to a blacklisted account', async () => { await expect( asset.methods.transfer_public(adminAddress, blacklistedAddress, 1n, 0n).simulate({ from: adminAddress }), diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts index 00e1722613f3..2feca0c06cc9 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts @@ -5,6 +5,9 @@ import { sendThroughAuthwitProxy, simulateThroughAuthwitProxy } from '../fixture import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from '../fixtures/fixtures.js'; import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; +// Covers the unshield (private→public) operation on TokenBlacklist, including authwit-delegated unshielding +// and blacklist enforcement on sender and recipient. Setup: single node with AutomineSequencer, 3 accounts, +// initial mint applied. Time-warp required during setup to cross role-change delay. describe('e2e_blacklist_token_contract unshielding', () => { const t = new BlacklistTokenContractTest('unshielding'); let { asset, tokenSim, wallet, adminAddress, otherAddress, blacklistedAddress } = t; @@ -25,6 +28,7 @@ describe('e2e_blacklist_token_contract unshielding', () => { await t.tokenSim.check(); }); + // Unshields half of admin's private balance to admin's public balance and verifies via TokenSimulator. it('on behalf of self', async () => { const balancePriv = await asset.methods .balance_of_private(adminAddress) @@ -38,6 +42,8 @@ describe('e2e_blacklist_token_contract unshielding', () => { tokenSim.transferToPublic(adminAddress, adminAddress, amount); }); + // Creates a private authwit for unshield, sends through proxy to other's public balance, verifies + // TokenSimulator, then asserts replay fails with DUPLICATE_NULLIFIER_ERROR. it('on behalf of other', async () => { const balancePriv0 = await asset.methods .balance_of_private(adminAddress) @@ -60,7 +66,9 @@ describe('e2e_blacklist_token_contract unshielding', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths: more-than-balance, invalid nonce, over-balance via authwit, wrong caller, blacklist. describe('failure cases', () => { + // Unshields more than private balance (self); expects 'Balance too low'. it('on behalf of self (more than balance)', async () => { const balancePriv = await asset.methods .balance_of_private(adminAddress) @@ -74,6 +82,7 @@ describe('e2e_blacklist_token_contract unshielding', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Self-unshield with nonce=1; expects the invalid-nonce assertion failure. it('on behalf of self (invalid authwit nonce)', async () => { const balancePriv = await asset.methods .balance_of_private(adminAddress) @@ -89,6 +98,7 @@ describe('e2e_blacklist_token_contract unshielding', () => { ); }); + // Authwit-unshields more than private balance via proxy; expects 'Balance too low'. it('on behalf of other (more than balance)', async () => { const balancePriv0 = await asset.methods .balance_of_private(adminAddress) @@ -107,6 +117,8 @@ describe('e2e_blacklist_token_contract unshielding', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Creates authwit designating otherAddress as caller but sends through proxy; expects unknown-authwit + // error because the message hash references the proxy address. it('on behalf of other (invalid designated caller)', async () => { const balancePriv0 = await asset.methods .balance_of_private(adminAddress) @@ -131,12 +143,14 @@ describe('e2e_blacklist_token_contract unshielding', () => { ).rejects.toThrow(`Unknown auth witness for message hash ${expectedMessageHash.toString()}`); }); + // Attempts unshield where the sender (from) is blacklisted; expects 'Blacklisted: Sender'. it('unshield from blacklisted account', async () => { await expect( asset.methods.unshield(blacklistedAddress, adminAddress, 1n, 0).simulate({ from: blacklistedAddress }), ).rejects.toThrow('Assertion failed: Blacklisted: Sender'); }); + // Attempts unshield where the recipient (to) is blacklisted; expects 'Blacklisted: Recipient'. it('unshield to blacklisted account', async () => { await expect( asset.methods.unshield(adminAddress, blacklistedAddress, 1n, 0).simulate({ from: adminAddress }), 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 1b519575ef68..eb52b960940f 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 @@ -33,6 +33,12 @@ import { setup } from './fixtures/utils.js'; import { TestWallet } from './test-wallet/test_wallet.js'; import { proveInteraction } from './test-wallet/utils.js'; +// Tests block building mechanics under the production sequencer with pipelining: +// multi-tx blocks, double-spend rejection, log ordering, regressions, and L1 reorgs. +// Uses setup() with PIPELINING_SETUP_OPTS (ethereumSlotDuration=4s, aztecSlotDuration=12s, +// minTxsPerBlock=0; aztecEpochDuration and aztecProofSubmissionEpochs are setup() defaults). +// The `reorgs` describe uses RollupCheatCodes (advanceToNextEpoch, markAsProven, advanceToEpoch) +// — other-active L1, not cross-chain bridging. CI job has TIMEOUT=25m. describe('e2e_block_building', () => { jest.setTimeout(20 * 60 * 1000); // 20 minutes @@ -51,6 +57,8 @@ describe('e2e_block_building', () => { jest.restoreAllMocks(); }); + // Tests assembling blocks with multiple simultaneous transactions under pipelining. + // setup(2, PIPELINING_SETUP_OPTS) with fast polling intervals; minTxsPerBlock set per test. describe('multi-txs block', () => { beforeAll(async () => { let sequencerClient: SequencerClient | undefined; @@ -95,6 +103,8 @@ describe('e2e_block_building', () => { // than fit in one sub-slot, the proposer must cut the block off at the deadline and roll the excess // txs into the next sub-slot (and the next checkpoint when the slot ends). It must NOT pack everything // into a single block and burn the whole slot on it. + // Configures BLOCK_DURATION_MS=2s and FAKE_DELAY_PER_TX=500ms, floods 10 txs, asserts they span + // at least 2 distinct blocks (sub-slot deadline enforced). it('processes txs until hitting timetable', async () => { // The timetable is always enforced. Fixture defaults under pipelining: aztecSlotDuration=12s, // ethereumSlotDuration=4s. With ethereumSlotDuration<8 the timing model normalizes to @@ -147,6 +157,8 @@ describe('e2e_block_building', () => { expect(unique(blockNumbers).length).toBeGreaterThanOrEqual(2); }); + // Sends 8 StatefulTestContract deploys simultaneously, waits for all to mine, and asserts + // all land in the same block with INITIALIZED status. it('assembles a block with multiple txs', async () => { // Assemble N contract deployment txs // We need to create them sequentially since we cannot have parallel calls to a circuit @@ -195,6 +207,8 @@ describe('e2e_block_building', () => { expect(areInitialized).toEqual(times(TX_COUNT, () => ContractInitializationStatus.INITIALIZED)); }); + // Sends 4 public increment_public_value calls simultaneously, waits for all to mine, + // and asserts all land in the same block. it('assembles a block with multiple txs with public fns', async () => { // First deploy the contract const { contract } = await StatefulTestContract.deploy(wallet, ownerAddress, 1).send({ from: ownerAddress }); @@ -258,6 +272,7 @@ describe('e2e_block_building', () => { }); // Uses priority fees to guarantee the deploy tx is ordered before the call tx within the same block. + // Sends two txs with different priority fees, asserts they both land in the same block. it('can call public function from different tx in same block as deployed', async () => { // Ensure both txs will land on the same block await aztecNodeAdmin.setConfig({ minTxsPerBlock: 2 }); @@ -298,6 +313,8 @@ describe('e2e_block_building', () => { }); }); + // Tests that duplicate nullifiers are rejected, both within the same block and across blocks. + // setup(1, PIPELINING_SETUP_OPTS), one node, production sequencer. describe('double-spends', () => { let contract: TestContract; let teardown: () => Promise; @@ -317,7 +334,10 @@ describe('e2e_block_building', () => { // Regressions for https://github.com/AztecProtocol/aztec-packages/issues/2502 // Note that the order in which the TX are processed is not guaranteed. + // Both txs race to the same block; exactly one succeeds and the other fails. describe('in the same block, different tx', () => { + // Sends two private emit_nullifier txs with the same nullifier simultaneously; + // asserts one succeeds and one rejects with DUPLICATE_NULLIFIER_ERROR. it('private <-> private', async () => { const nullifier = Fr.random(); const txs = await sendAndWait( @@ -335,6 +355,7 @@ describe('e2e_block_building', () => { ]); }); + // Same as private<->private but both txs use public nullifier emission. it('public -> public', async () => { const nullifier = Fr.random(); const txs = await sendAndWait( @@ -352,6 +373,7 @@ describe('e2e_block_building', () => { ]); }); + // One private and one public tx emit the same nullifier simultaneously; one must fail. it('private -> public', async () => { const nullifier = Fr.random(); const txs = await sendAndWait( @@ -369,6 +391,7 @@ describe('e2e_block_building', () => { ]); }); + // One public and one private tx emit the same nullifier simultaneously; one must fail. it('public -> private', async () => { const nullifier = Fr.random(); const txs = await sendAndWait( @@ -387,7 +410,9 @@ describe('e2e_block_building', () => { }); }); + // Double-spend rejection when the second tx arrives in a later block (nullifier already in the tree). describe('across blocks', () => { + // Emits a private nullifier, then tries to emit the same in a subsequent tx and expects rejection. it('private -> private', async () => { const nullifier = Fr.random(); await contract.methods.emit_nullifier(nullifier).send({ from: ownerAddress }); @@ -396,6 +421,7 @@ describe('e2e_block_building', () => { ); }); + // Emits a public nullifier, then tries again in a subsequent tx and expects rejection. it('public -> public', async () => { const nullifier = Fr.random(); await contract.methods.emit_nullifier_public(nullifier).send({ from: ownerAddress }); @@ -404,6 +430,7 @@ describe('e2e_block_building', () => { ); }); + // Emits via private then tries public with the same nullifier in a later block; expects rejection. it('private -> public', async () => { const nullifier = Fr.random(); await contract.methods.emit_nullifier(nullifier).send({ from: ownerAddress }); @@ -412,6 +439,7 @@ describe('e2e_block_building', () => { ); }); + // Emits via public then tries private with the same nullifier in a later block; expects rejection. it('public -> private', async () => { const nullifier = Fr.random(); await contract.methods.emit_nullifier_public(nullifier).send({ from: ownerAddress }); @@ -422,6 +450,8 @@ describe('e2e_block_building', () => { }); }); + // Verifies that private encrypted logs and unencrypted logs emitted from nested calls are ordered + // correctly in the block. setup(1, PIPELINING_SETUP_OPTS). describe('logs in nested calls are ordered as expected', () => { // This test was originally written for e2e_nested, but it was refactored // to not use TestContract. @@ -442,6 +472,8 @@ describe('e2e_block_building', () => { afterAll(() => teardown()); + // Sends emit_array_as_encrypted_log, retrieves ExampleEvent private logs and a raw siloed log, + // and asserts ordering and field values are correct. it('calls a method with nested encrypted logs', async () => { const values = { value0: 5n, @@ -493,6 +525,7 @@ describe('e2e_block_building', () => { }, 60_000); }); + // Regression tests for specific sequencer bugs; each creates its own setup(). describe('regressions', () => { afterEach(async () => { if (teardown) { @@ -501,6 +534,7 @@ describe('e2e_block_building', () => { }); // Regression for https://github.com/AztecProtocol/aztec-packages/issues/7918 + // Waits for block number >= 3 with buildCheckpointIfEmpty=true to confirm empty checkpoints are built. it('publishes two empty blocks', async () => { ({ teardown, wallet, logger, aztecNode } = await setup(0, { ...PIPELINING_SETUP_OPTS, @@ -510,10 +544,12 @@ describe('e2e_block_building', () => { // Under pipelining, with `aztecSlotDuration=12s`, each empty checkpoint contains one empty // block and lands roughly every 12s. Allow up to 60s for three empty blocks to appear. + // REFACTOR: raw retryUntil poll on block number; replace with a waitForBlock(n) DSL helper await retryUntil(async () => (await aztecNode.getBlockNumber()) >= 3, 'wait-block', 60, 1); }); // Regression for https://github.com/AztecProtocol/aztec-packages/issues/7537 + // Deploys an account on block 1 with minTxsPerBlock=0 to verify the first block can accept txs. it('sends a tx on the first block', async () => { const context = await setup(0, { ...PIPELINING_SETUP_OPTS, @@ -521,6 +557,7 @@ describe('e2e_block_building', () => { additionallyFundedAccounts: await generateSchnorrAccounts(1, 'schnorr'), }); ({ teardown, logger, aztecNode, wallet } = context); + // REFACTOR: sleep-based wait; replace with a waitForBlock(1) or equivalent readiness helper await sleep(1000); const [accountData] = context.additionallyFundedAccounts; @@ -532,6 +569,7 @@ describe('e2e_block_building', () => { }); }); + // Floods 24 Token.mint_to_public txs while the sequencer is building blocks and asserts all land. it('can simulate public txs while building a block', async () => { ({ teardown, @@ -571,6 +609,7 @@ describe('e2e_block_building', () => { // The culprit is a nullifier not being cleared up from world state during block building if a tx fails processing, // which translates in an incorrect end state for world state. We can easily detect this by checking whether the nullifier // tree next available leaf index is a multiple of 64. + // Injects a fakeThrowAfterProcessingTxCount=2 to force AVM failure, verifies nullifier tree alignment. it('clears up all nullifiers if tx processing fails', async () => { const context = await setup(1, { ...PIPELINING_SETUP_OPTS, minTxsPerBlock: 1 }); ({ @@ -624,6 +663,9 @@ describe('e2e_block_building', () => { }); }); + // Tests that the sequencer handles L2 reorgs correctly: detects stale proofs, prunes affected txs, + // and re-includes those that were built against a proven block. + // Uses cheatCodes.rollup.advanceToNextEpoch, markAsProven, advanceToEpoch, retryUntil. describe('reorgs', () => { let contract: StatefulTestContract; let cheatCodes: CheatCodes; @@ -652,11 +694,15 @@ describe('e2e_block_building', () => { // interval mining, so we drive proven manually here (and again inside each test). await cheatCodes.rollup.markAsProven(); const bn = await aztecNode.getBlockNumber(); + // REFACTOR: raw retryUntil poll on proven block number; replace with waitForProvenBlock(n) helper await retryUntil(async () => (await aztecNode.getBlockNumber('proven')) >= bn, 'wait-proven', 60, 1); }); afterEach(() => teardown()); + // Advances epoch, marks proven, sends two txs, then advances past the proof-submission window + // causing a reorg. Waits for tx1 to be pruned then re-included at the same block number. + // Asserts tx2 is dropped, tx1 is re-included, and a subsequent tx lands cleanly. it('detects an upcoming reorg and builds a block for the correct slot', async () => { // Advance to a fresh epoch and mark the current one as proven await cheatCodes.rollup.advanceToNextEpoch(); @@ -683,6 +729,7 @@ describe('e2e_block_building', () => { // Wait until the sequencer kicks out tx1 logger.info(`Waiting for node to prune tx1`); + // REFACTOR: raw retryUntil polling for tx status transition; replace with a waitForTxPruned() helper await retryUntil( async () => (await aztecNode.getTxReceipt(tx1.txHash)).status === TxStatus.PENDING, 'wait for pruning', @@ -692,6 +739,7 @@ describe('e2e_block_building', () => { // And wait until it is brought back tx1 logger.info(`Waiting for node to re-include tx1`); + // REFACTOR: raw retryUntil polling for re-inclusion; replace with a waitForTxReincluded() helper await retryUntil( async () => { const receipt = await aztecNode.getTxReceipt(tx1.txHash); 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 514746bea348..f475b5e4fc49 100644 --- a/yarn-project/end-to-end/src/e2e_bot.test.ts +++ b/yarn-project/end-to-end/src/e2e_bot.test.ts @@ -24,6 +24,12 @@ import { jest } from '@jest/globals'; import { PIPELINED_FEE_PADDING, PIPELINING_SETUP_OPTS } from './fixtures/fixtures.js'; import { getPrivateKeyFromIndex, setup } from './fixtures/utils.js'; +// Tests the transaction bot implementations (transfer bot, AMM bot, cross-chain bot). +// Uses setup(0, PIPELINING_SETUP_OPTS + aztecProofSubmissionEpochs:640) with one node, production +// sequencer (ethereumSlotDuration=4s, aztecSlotDuration=12s, proofSubEpochs=640, minTxsPerBlock=0; +// aztecEpochDuration is the setup() default). The bridge-resume, setup-via-bridging, and +// cross-chain-bot subsuites actively drive L1 cross-chain bridging: fee-juice portal deposits, +// advanceInboxInProgress, and L2→L1 messages via CrossChainBot. describe('e2e_bot', () => { let wallet: EmbeddedWallet; let aztecNode: AztecNode; @@ -56,6 +62,7 @@ describe('e2e_bot', () => { let privateKeyIndex = 10; const getPrivateKey = () => new SecretValue(bufferToHex(getPrivateKeyFromIndex(privateKeyIndex++)!)); + // Tests the default Token-transfer Bot: send transfers, hardcoded-gas mode, and contract reuse. describe('transaction-bot', () => { let bot: Bot; beforeAll(async () => { @@ -68,6 +75,7 @@ describe('e2e_bot', () => { bot = await Bot.create(config, wallet, aztecNode, undefined, new BotStore(await openTmpStore('bot'))); }); + // Runs bot.run() once and asserts recipient private and public balances each increase by 1. it('sends token transfers from the bot', async () => { const { recipient: recipientBefore } = await bot.getBalances(); @@ -77,6 +85,7 @@ describe('e2e_bot', () => { expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); }); + // Updates bot config to use max DA and L2 gas limits (no simulation), runs, asserts balances +1. it('sends token transfers with hardcoded gas and no simulation', async () => { bot.updateConfig({ daGasLimit: MAX_TX_DA_GAS, l2GasLimit: MAX_PROCESSABLE_L2_GAS }); const { recipient: recipientBefore } = await bot.getBalances(); @@ -87,6 +96,8 @@ describe('e2e_bot', () => { expect(recipientAfter.publicBalance - recipientBefore.publicBalance).toEqual(1n); }); + // Creates a second Bot instance with the same config and asserts it resolves the same + // sender address and token contract as the first. it('reuses the same token contract', async () => { const { defaultAccountAddress, token } = bot; const bot2 = await Bot.create(config, wallet, aztecNode, undefined, new BotStore(await openTmpStore('bot'))); @@ -94,6 +105,7 @@ describe('e2e_bot', () => { expect(bot2.token.address.toString()).toEqual(token.address.toString()); }); + // Creates a bot using PrivateTokenContract variant and verifies only private balance changes. it('sends token from the bot using PrivateToken', async () => { const easyBot = await Bot.create( { ...config, contract: SupportedTokenContracts.PrivateTokenContract }, @@ -111,6 +123,9 @@ describe('e2e_bot', () => { }); }); + // Tests that a partially-completed fee-juice bridge claim is persisted in BotStore and + // reused (not re-bridged) on a subsequent Bot.create call. Also verifies that a different + // recipient address invalidates the stored claim. Actively drives L1 (fee juice bridging). describe('bridge resume', () => { let store: BotStore; @@ -122,6 +137,8 @@ describe('e2e_bot', () => { await store.close(); }); + // First Bot.create call fails at deploy (mocked) after saving a bridge claim. Second call + // succeeds without re-bridging (saveBridgeClaim not called again). it('reuses prior bridge claims', async () => { using saveSpy = jest.spyOn(store, 'saveBridgeClaim'); const config: BotConfig = { @@ -165,6 +182,8 @@ describe('e2e_bot', () => { } }); + // Changes the sender salt between attempts; asserts a new bridge claim is triggered even though + // the prior claim is in the store. it('does not reuse prior bridge claims if recipient address changes', async () => { using saveSpy = jest.spyOn(store, 'saveBridgeClaim'); const config: BotConfig = { @@ -208,6 +227,8 @@ describe('e2e_bot', () => { }); }); + // Tests the AmmBot: swaps a random token direction and verifies one private balance decreased + // and one increased. describe('amm-bot', () => { let bot: AmmBot; beforeAll(async () => { @@ -219,6 +240,8 @@ describe('e2e_bot', () => { bot = await AmmBot.create(config, wallet, aztecNode, undefined, new BotStore(await openTmpStore('bot'))); }); + // Runs the AMM bot once and asserts one of the two private token balances decreased and + // the other increased (direction is random). it('swaps tokens from the bot', async () => { const balancesBefore = await bot.getBalances(); await expect(bot.run()).resolves.toBeDefined(); @@ -239,6 +262,8 @@ describe('e2e_bot', () => { }); }); + // Tests that Bot.create succeeds after the inbox drifts away from the rollup contract. + // Actively drives L1 via advanceInboxInProgress. describe('setup via bridging funds cross-chain', () => { beforeAll(() => { config = { @@ -254,12 +279,15 @@ describe('e2e_bot', () => { // See 'can consume L1 to L2 message in %s after inbox drifts away from the rollup' // in end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts for context on this test. + // Advances inbox 4 slots then creates Bot; verifies it completes setup without error. it('creates bot after inbox drift', async () => { await cheatCodes.rollup.advanceInboxInProgress(4); await Bot.create(config, wallet, aztecNode, aztecNodeAdmin, new BotStore(await openTmpStore('bot'))); }, 300_000); }); + // Tests the CrossChainBot: seeds L1→L2 messages and on each tick consumes one while seeding + // a replacement. Actively drives L1 portal contracts. describe('cross-chain-bot', () => { let bot: CrossChainBot; @@ -282,6 +310,8 @@ describe('e2e_bot', () => { ); }, 600_000); + // Runs the cross-chain bot once; asserts a MinedTxReceipt is returned and the mined block + // contains at least one non-zero L2→L1 message. it('sends L2→L1 and consumes L1→L2 messages', async () => { const result = await bot.run(); expect(result).toBeDefined(); @@ -297,6 +327,8 @@ describe('e2e_bot', () => { expect(l2ToL1Msgs.length).toBeGreaterThanOrEqual(1); }, 300_000); + // Second bot.run() tick; asserts the result is defined, confirming the pipeline replenishment + // from the first tick allows a second immediate consumption. it('replenishes the seeding pipeline across ticks', async () => { // Tick 2: the first tick consumed one message. This tick should seed a // replacement and still have a ready message to consume. diff --git a/yarn-project/end-to-end/src/e2e_card_game.test.ts b/yarn-project/end-to-end/src/e2e_card_game.test.ts index 1c9b9a00653a..c24c74a07988 100644 --- a/yarn-project/end-to-end/src/e2e_card_game.test.ts +++ b/yarn-project/end-to-end/src/e2e_card_game.test.ts @@ -56,6 +56,12 @@ const GAME_ID = 42; const TIMEOUT = 600_000; +// End-to-end test for the CardGame contract: buying packs, joining games, playing rounds, +// claiming won cards. Uses setup(0, AUTOMINE_E2E_OPTS, additionallyFundedAccounts: 3 players) with one +// node, automine sequencer, and three funded accounts (players) the test generates and creates itself as +// initializerless accounts — it derives nullifier-hiding keys from the players' secrets, so it owns the +// keys. (v5: was setup(3, …); the explicit account provisioning is a setup-mechanics change, not a +// category change.) jest.setTimeout(600s). describe('e2e_card_game', () => { jest.setTimeout(TIMEOUT); @@ -115,6 +121,8 @@ describe('e2e_card_game', () => { logger.info(`L2 contract deployed at ${contract.address}`); }; + // Calls buy_pack for firstPlayer with seed=27, then reads the collection and compares against + // the TS-reproduced card generation logic. it('should be able to buy packs', async () => { const seed = 27n; // docs:start:send_tx @@ -127,6 +135,7 @@ describe('e2e_card_game', () => { expect(boundedVecToArray(collection)).toMatchObject(expected); }); + // Tests joining a game: buying packs for two players and verifying game state after joining. describe('game join', () => { const seed = 27n; let firstPlayerCollection: Card[]; @@ -141,6 +150,8 @@ describe('e2e_card_game', () => { ); }); + // First player joins with two specific cards; second player tries to join with an already-played + // card from first player and expects a revert. Verifies game state reflects only first player. it('should be able to join games', async () => { await contract.methods .join_game(GAME_ID, [cardToField(firstPlayerCollection[0]), cardToField(firstPlayerCollection[2])]) @@ -178,6 +189,7 @@ describe('e2e_card_game', () => { }); }); + // Both players join, first player calls start_game, verifies game state has both players and started=true. it('should start games', async () => { const secondPlayerCollection = boundedVecToArray( (await contract.methods.view_collection_cards(secondPlayer, 0).simulate({ from: secondPlayer })) @@ -216,6 +228,8 @@ describe('e2e_card_game', () => { }); }); + // Full happy-path game play: three players buy packs, two play a game, winner claims cards + // and plays a second match. describe('game play', () => { let firstPlayerCollection: Card[]; let secondPlayerCollection: Card[]; @@ -270,6 +284,8 @@ describe('e2e_card_game', () => { return finalGameState; } + // Two players join and start a game; all rounds are played; winner claims won cards; loser cannot + // claim; winner and thirdPlayer play a second game using the won cards. it('should play a game, claim the winned cards and play another match with winned cards', async () => { const firstPlayerGameDeck = [firstPlayerCollection[0], firstPlayerCollection[2]]; const secondPlayerGameDeck = [secondPlayerCollection[0], secondPlayerCollection[2]]; diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index b66f73c51a87..9f7fbf10a26c 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -13,7 +13,11 @@ import { foundry } from 'viem/chains'; import { MNEMONIC } from './fixtures/fixtures.js'; import { getLogger, startAnvil } from './fixtures/utils.js'; +// Tests the EthCheatCodes API directly against a standalone anvil instance (no Aztec node). +// Does NOT use setup(); starts anvil directly via startAnvil(). Single-file, no L2 stack. describe('e2e_cheat_codes', () => { + // Tests L1 anvil cheat-code primitives: mine, timestamp manipulation, storage load/store, + // bytecode patching, and account impersonation. Each test gets a fresh anvil. describe('L1 cheatcodes', () => { let ethCheatCodes: EthCheatCodes; @@ -31,13 +35,16 @@ describe('e2e_cheat_codes', () => { afterEach(async () => await anvil?.stop().catch(err => getLogger().error(err))); + // Tests that ethCheatCodes.mine() and mine(n) advance the L1 block number by 1 and n respectively. describe('mine', () => { + // Calls mine() and asserts block number advances by exactly 1. it(`mine block`, async () => { const blockNumber = await ethCheatCodes.blockNumber(); await ethCheatCodes.mine(); expect(await ethCheatCodes.blockNumber()).toBe(blockNumber + 1); }); + // Calls mine(n) for n in [10, 42, 99] and asserts block number advances by n. it.each([10, 42, 99])(`mine %i blocks`, async increment => { const blockNumber = await ethCheatCodes.blockNumber(); await ethCheatCodes.mine(increment); @@ -45,6 +52,7 @@ describe('e2e_cheat_codes', () => { }); }); + // Sets next block timestamp forward by increment, mines, and asserts the new timestamp matches. it.each([100, 42, 99])(`setNextBlockTimestamp by %i`, async increment => { const blockNumber = await ethCheatCodes.blockNumber(); const timestamp = await ethCheatCodes.lastBlockTimestamp(); @@ -58,6 +66,7 @@ describe('e2e_cheat_codes', () => { expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp + increment); }); + // Attempts to set a timestamp in the past and expects a "Timestamp error" rejection. it('setNextBlockTimestamp to a past timestamp throws', async () => { const timestamp = await ethCheatCodes.lastBlockTimestamp(); const pastTimestamp = timestamp - 1000; @@ -66,12 +75,14 @@ describe('e2e_cheat_codes', () => { ); }); + // Loads storage slot 0 from the zero address and confirms it is 0. it('load a value at a particular storage slot', async () => { // check that storage slot 0 is empty as expected const res = await ethCheatCodes.load(EthAddress.ZERO, 0n); expect(res).toBe(0n); }); + // Stores a value at a given slot and at its keccak256 map-derived slot, then loads and verifies. it.each(['1', 'bc40fbf4394cd00f78fae9763b0c2c71b21ea442c42fdadc5b720537240ebac1'])( 'store a value at a given slot and its keccak value of the slot (if it were in a map) ', async storageSlotInHex => { @@ -86,12 +97,15 @@ describe('e2e_cheat_codes', () => { }, ); + // Patches an account's bytecode with etch() and reads it back to confirm. it('set bytecode correctly', async () => { const contractAddress = EthAddress.fromString('0x70997970C51812dc3A010C7d01b50e0d17dc79C8'); await ethCheatCodes.etch(contractAddress, '0x1234'); expect(await ethCheatCodes.getBytecode(contractAddress)).toBe('0x1234'); }); + // Funds a random address, impersonates it to send ETH, stops impersonation, then confirms + // sending from the address again fails with "No Signer available". it('impersonate', async () => { // we will transfer 1 eth to a random address. Then impersonate the address to be able to send funds // without impersonation we wouldn't be able to send funds. diff --git a/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts index 8d3e65140dad..d5b79f23eb35 100644 --- a/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts +++ b/yarn-project/end-to-end/src/e2e_circuit_recorder.test.ts @@ -10,9 +10,17 @@ import { setup } from './fixtures/utils.js'; /** * Tests the circuit recorder is working as expected. To read more about it, check JSDoc of CircuitRecorder class. */ +// Tests that setting CIRCUIT_RECORD_DIR activates the CircuitRecorder and produces recording files +// for both user circuits (the SchnorrInitializerlessAccount entrypoint exercised by deploying a +// ChildContract) and protocol circuits (PrivateKernelInit variant). (v5: the default account is now +// initializerless, so the user circuit recorded is the entrypoint, not a SchnorrAccount constructor.) +// Uses setup(1, AUTOMINE_E2E_OPTS) with one node, automine sequencer, one account. describe('Circuit Recorder', () => { const RECORD_DIR = './circuit_recordings'; + // Sets CIRCUIT_RECORD_DIR env var, runs setup + a ChildContract deploy to trigger circuit execution, + // then asserts recording files exist for SchnorrInitializerlessAccount_entrypoint and a + // PrivateKernelInit variant. it('records circuit execution', async () => { // Set recording directory env var - this will activate the circuit recorder process.env.CIRCUIT_RECORD_DIR = RECORD_DIR; diff --git a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts index 0780a440151c..4092c1fce247 100644 --- a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts +++ b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts @@ -34,6 +34,12 @@ const INITIAL_UPDATABLE_CONTRACT_VALUE = 1n; // Constant copied over from Updated contract const UPDATED_CONTRACT_PUBLIC_VALUE = 27n; +// Tests the contract class update mechanism: scheduling an upgrade, time-warping past the delay, +// and verifying the new class is active. Also tests simulation overrides for post-upgrade calls. +// Uses setup(0, AUTOMINE_E2E_OPTS) with genesisPublicData and a deterministic initializerless account +// in additionallyFundedAccounts (whose address is known before setup so the delay can be seeded in +// genesis for it). (v5: was setup(1, …) with initialFundedAccounts; the renamed option and +// initializerless account are setup-mechanics changes, not category changes.) describe('e2e_contract_updates', () => { let wallet: TestWallet; let defaultAccountAddress: AztecAddress; @@ -113,6 +119,8 @@ describe('e2e_contract_updates', () => { afterEach(() => teardown()); + // Schedules an update to UpdatedContractClassId, warps L2 time past DEFAULT_TEST_UPDATE_DELAY, + // then calls new private and public methods only available in the updated class. it('should update the contract', async () => { expect( (await contract.methods.get_private_value(defaultAccountAddress).simulate({ from: defaultAccountAddress })) @@ -144,6 +152,8 @@ describe('e2e_contract_updates', () => { ); }); + // Increases the delay by 1, schedules an update, warps past the new delay, verifies the update + // took effect. it('should change the update delay and then update the contract', async () => { expect((await contract.methods.get_update_delay().simulate({ from: defaultAccountAddress })).result).toEqual( BigInt(DEFAULT_TEST_UPDATE_DELAY), @@ -168,12 +178,15 @@ describe('e2e_contract_updates', () => { await updatedContract.methods.set_private_value().send({ from: defaultAccountAddress }); }); + // Tries to set a delay below MINIMUM_UPDATE_DELAY and expects a revert "New update delay is too low". it('should not allow to change the delay to a value lower than the minimum', async () => { await expect( contract.methods.set_update_delay(BigInt(MINIMUM_UPDATE_DELAY) - 1n).simulate({ from: defaultAccountAddress }), ).rejects.toThrow('New update delay is too low'); }); + // Tries to register the instance against UpdatedContract.artifact before the upgrade window passes; + // expects the PXE to reject with a class mismatch error. it('should not allow to instantiate a contract with an updated class before the update happens', async () => { await expect(wallet.registerContract(instance, UpdatedContract.artifact)).rejects.toThrow( 'Could not update contract to a class different from the current one', @@ -184,6 +197,7 @@ describe('e2e_contract_updates', () => { // have different function selectors. Without an upgrade, only the deployed Updatable's // (Field) selector exists; with a fastForwardContractUpdate override, the AVM dispatches // against UpdatedContract's bytecode and the no-args selector resolves. + // Asserts that without overrides the call fails, with overrides it succeeds, and real storage is unaffected. it('fastForwardContractUpdate enables simulation of post-upgrade public calls', async () => { // Local construction with the new artifact - no PXE/wallet side effect, no chain mutation. const updatedContract = UpdatedContract.at(contract.address, wallet); @@ -213,6 +227,7 @@ describe('e2e_contract_updates', () => { // UpdatedContract.set_private_value is a private function that doesn't exist on UpdatableContract. // For PXE-side ACIR dispatch to find it, the artifact must be registered locally first via // wallet.registerContractClass; the helper itself only takes the class id. + // Asserts that without local artifact registration the call fails, with it the call succeeds under overrides. it('fastForwardContractUpdate enables simulation of post-upgrade private calls', async () => { const updatedContract = UpdatedContract.at(contract.address, wallet); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.parallel.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.parallel.test.ts index ae3a689173cb..618a94a1d822 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.parallel.test.ts @@ -21,6 +21,11 @@ import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; jest.setTimeout(300_000); +// L1→L2 messaging via Inbox: message readiness, duplicate messages, and inbox drift after a rollup +// reorg. Uses CrossChainMessagingTest (prod sequencer, pipelining preset: ethSlot=4s, aztecSlot=12s, +// inboxLag=2, minTxsPerBlock=1, aztecProofSubmissionEpochs=2, aztecEpochDuration=4) with +// EpochTestSettler for auto-proving and CrossChainTestHarness for L1↔L2 token portal bridging. +// Each it is run as an independent CI job (*.parallel.test.ts convention). describe('e2e_cross_chain_messaging l1_to_l2', () => { let t: CrossChainMessagingTest; let log: Logger; @@ -90,6 +95,8 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { return newBlock; }; + // REFACTOR: hand-rolled retryUntil polling for a block to reach the checkpointed chain tip; replace + // with a shared waitForBlockCheckpointed(node, blockNumber) helper in the e2e fixture utilities. const waitForBlockToCheckpoint = async (blockNumber: BlockNumber) => { return await retryUntil( async () => { @@ -131,6 +138,8 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { }; // Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint + // REFACTOR: hand-rolled retryUntil loop that also advances blocks on each retry; replace with a + // waitForL1ToL2MessageIndexed(node, msgHash, advanceBlock) helper in the e2e fixture or harness. const waitForMessageFetched = async (msgHash: Fr) => { log.warn(`Waiting until the message is fetched by the node`); return await retryUntil( @@ -226,10 +235,15 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { await sendConsumeMsgTx(actualMessage2Index); }; + // Sends the same L1→L2 message content twice via a non-registered portal, waits for each to be + // ready, and consumes both from private. Verifies duplicate messages are indexed correctly and + // the second consumption uses the non-nullified duplicate leaf. it('can send an L1 to L2 message from a non-registered portal address consumed from private repeatedly', async () => { await canSendMessageFromNonRegisteredPortal('private'); }); + // Same as above but the message is consumed from public state. Verifies the public consumption + // path handles duplicate messages and the oracle returns the correct non-nullified leaf index. it('can send an L1 to L2 message from a non-registered portal address consumed from public repeatedly', async () => { await canSendMessageFromNonRegisteredPortal('public'); }); @@ -329,10 +343,15 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { } }; + // Mines four checkpoints without proving, inserting an L1→L2 message after the drift, then + // triggers a rollup prune back to the pre-drift block. Verifies the message can be consumed from + // private only after the chain re-syncs to the message's checkpoint, not before. it('can consume L1 to L2 message in private after inbox drifts away from the rollup', async () => { await canConsumeMessageAfterInboxDrift('private'); }); + // Same drift scenario but consuming from public. Uses a send+dontThrowOnRevert loop to probe when + // the message becomes consumable, then verifies the successful tx is in the message's checkpoint. it('can consume L1 to L2 message in public after inbox drifts away from the rollup', async () => { await canConsumeMessageAfterInboxDrift('public'); }); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts index 3a54c4bd9e90..cda2e2537f46 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l2_to_l1.test.ts @@ -24,6 +24,8 @@ import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; * the next checkpoint job rather than racing with an in-flight one. Mirrors the helper in * `e2e_fees/gas_estimation.test.ts`. */ +// REFACTOR: duplicated from e2e_fees/gas_estimation.test.ts; extract to a shared fixture helper +// (e.g. waitForSequencerState) so both call sites can share it without copy-pasting. function waitForSequencerIdle(sequencer: Sequencer, timeout = 30000): Promise { if (sequencer.status().state === SequencerState.IDLE) { return Promise.resolve(); @@ -44,6 +46,10 @@ function waitForSequencerIdle(sequencer: Sequencer, timeout = 30000): Promise { // Pipelining slows wall-clock chain progress (12s slots); advanceToEpochProven plus the per-test // multi-tx flows exceed the default 300s per-test budget. @@ -84,6 +90,9 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { // Note: We register one portal address when deploying contract but that address is no-longer the only address // allowed to receive messages from the given contract. In the following test we'll test that it's really the case. + // Sends one tx with two L2→L1 messages (one from private, one from public) to a non-registered portal. + // Proves the epoch, then consumes both messages from L1 via the Outbox and asserts the MessageConsumed + // event is emitted and the message cannot be consumed a second time. it('1 tx with 2 messages, one from public, one from private, to a non-registered portal address', async () => { const recipient = crossChainTestHarness.ethAccount; const contents = [Fr.random(), Fr.random()]; @@ -183,6 +192,8 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { // When the block contains a tx with no messages, the zero txOutHash is skipped and won't be included in the top tree. // In this test, we test that the correct tree class is used, and the final out hash equals the only message leaf. + // Two txs packed into the same block: one emitting an L2→L1 message, one with no messages. Verifies + // the message tree is built correctly (zero txOutHash skipped) and the single message is consumable. it('2 txs in the same block, one with no messages, one with a message', async () => { const content = Fr.random(); const recipient = msgSender; @@ -210,6 +221,8 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { await expectConsumeMessageToSucceed(message, withMessageReceipt.txHash); }); + // Two txs with 3 and 4 messages respectively. Verifies the mixed-height subtree structure is + // built correctly and representative messages from each tx are consumable after epoch proving. it('2 txs (balanced), one with 3 messages (unbalanced), one with 4 messages (balanced)', async () => { // Force txs to be in the same block. await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 2 }); @@ -263,6 +276,8 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { } }); + // Three txs with 3, 1, and 2 messages. The 1-message tx's subtree root is the leaf itself; the + // 3-message tx is unbalanced. Verifies representative messages from each tx are consumable. it('3 txs (unbalanced), one with 3 messages (unbalanced), one with 1 message (the subtree root), one with 2 messages (balanced)', async () => { // Force txs to be in the same block. await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 3 }); @@ -317,9 +332,10 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { } }); - // Two txs, each emitting one L2-to-L1 message, packed into separate blocks of a single checkpoint. - // This exercises the checkpoint level of the L2-to-L1 message tree (the block out hashes within a - // checkpoint), which the single-block-per-checkpoint cases above never reach. See #17027. + // Two txs, each with one message, packed into separate blocks of the same checkpoint. Exercises the + // checkpoint-level L2→L1 tree (block out hashes within a checkpoint), which the single-block + // cases above never reach (see #17027). Membership witnesses span the checkpoint's block subtree; + // verifies both messages are consumable after epoch proving. it('2 txs each with a message, in different blocks of the same checkpoint', async () => { const recipient = msgSender; const contents = [Fr.random(), Fr.random()]; diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_failure_cases.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_failure_cases.test.ts index 5ec2e47899ce..3f8c1dd13858 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_failure_cases.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_failure_cases.test.ts @@ -8,6 +8,10 @@ import { toFunctionSelector } from 'viem'; import { L1_DIRECT_WRITE_ACCOUNT_INDEX, NO_L1_TO_L2_MSG_ERROR, PIPELINING_SETUP_OPTS } from '../fixtures/fixtures.js'; import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; +// Token bridge failure scenarios: missing authwit, wrong secret hash, and wrong deposit direction. +// Uses CrossChainMessagingTest (prod sequencer, pipelining preset: ethSlot=4s, aztecSlot=12s, +// inboxLag=2, minTxsPerBlock=0), EpochTestSettler for auto-proving, and CrossChainTestHarness for +// L1↔L2 token portal bridging. describe('e2e_cross_chain_messaging token_bridge_failure_cases', () => { const t = new CrossChainMessagingTest('token_bridge_failure_cases', {}, {}, {}, L1_DIRECT_WRITE_ACCOUNT_INDEX); let version: number = 1; @@ -28,6 +32,8 @@ describe('e2e_cross_chain_messaging token_bridge_failure_cases', () => { await t.teardown(); }); + // Attempts to call exit_to_l1_public without granting an authwit to the bridge contract. + // Asserts the simulation reverts with "unauthorized". it("Bridge can't withdraw my funds if I don't give approval", async () => { const mintAmountToOwner = 100n; await crossChainTestHarness.mintTokensPublicOnL2(mintAmountToOwner); @@ -42,6 +48,8 @@ describe('e2e_cross_chain_messaging token_bridge_failure_cases', () => { ).rejects.toThrow(/unauthorized/); }, 180_000); + // Sends a public deposit to the portal, then tries to claim with a wrong bridge amount, producing + // a mismatched message hash. Asserts "No L1 to L2 message found" for the wrong hash. it("Can't claim funds privately which were intended for public deposit from the token portal", async () => { const bridgeAmount = 100n; @@ -74,6 +82,8 @@ describe('e2e_cross_chain_messaging token_bridge_failure_cases', () => { ).rejects.toThrow(`No L1 to L2 message found for message hash ${wrongMessage.hash().toString()}`); }, 180_000); + // Sends a private deposit to the portal, then tries to claim it publicly using claim_public. + // The message hash does not match the public-mint selector, so consumption fails with NO_L1_TO_L2_MSG_ERROR. it("Can't claim funds publicly which were intended for private deposit from the token portal", async () => { // 1. Mint tokens on L1 const bridgeAmount = 100n; diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts index 150779b6b6e7..5ccdec25ae99 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_private.test.ts @@ -13,6 +13,10 @@ import type { CrossChainTestHarness } from '../shared/cross_chain_test_harness.j import type { TestWallet } from '../test-wallet/test_wallet.js'; import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; +// Private L1→L2 token deposit and L2→L1 withdrawal via the TokenBridge. Uses CrossChainMessagingTest +// with startProverNode=true (prod sequencer, pipelining preset: ethSlot=4s, aztecSlot=12s), fake +// in-proc prover node, and CrossChainTestHarness for full L1↔L2 portal/bridge lifecycle. +// Epoch proving via advanceToEpochProven is required before L1 Outbox consumption. describe('e2e_cross_chain_messaging token_bridge_private', () => { // Pipelining slows wall-clock chain progress (12s slots); waitForProven via advanceToEpochProven // needs more than the default 300s per-test budget. @@ -47,6 +51,9 @@ describe('e2e_cross_chain_messaging token_bridge_private', () => { await t.teardown(); }); + // Full round-trip: mint tokens on L1, deposit privately via TokenPortal, wait for the message to be + // consumable, claim on L2 (minting private tokens), withdraw back to L1 with an authwit, advance the + // epoch until proven, then consume the Outbox message on L1 and verify the L1 balance is restored. it('Privately deposit funds from L1 -> L2 and withdraw back to L1', async () => { // Generate a claim secret using pedersen const l1TokenBalance = 1000000n; @@ -88,6 +95,8 @@ describe('e2e_cross_chain_messaging token_bridge_private', () => { // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. await t.advanceToEpochProven(l2TxReceipt); + // REFACTOR: hand-rolled retryUntil polling for L2→L1 membership witness; replace with a + // waitForL2ToL1MessageWitness(node, txHash, leaf) helper shared across bridge tests. const l2ToL1MessageResult = await retryUntil( () => aztecNode.getL2ToL1MembershipWitness(l2TxReceipt.txHash, l2ToL1Message), 'l2 to l1 membership witness', @@ -108,6 +117,8 @@ describe('e2e_cross_chain_messaging token_bridge_private', () => { }); // This test checks that it's enough to have the claim secret to claim the funds to whoever we want. + // User2 (not the original depositor) uses the claim secret to call claim_private on behalf of owner. + // Asserts the funds land at ownerAddress (not user2), proving the secret-based authorization works. it('Claim secret is enough to consume the message', async () => { const initialPublicBalance = await crossChainTestHarness.getL1BalanceOf(ethAccount); const initialPrivateBalance = await crossChainTestHarness.getL2PrivateBalanceOf(ownerAddress); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts index 86f0ad96b336..d7c20f7e0853 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/token_bridge_public.test.ts @@ -6,6 +6,10 @@ import { jest } from '@jest/globals'; import { L1_DIRECT_WRITE_ACCOUNT_INDEX, NO_L1_TO_L2_MSG_ERROR, PIPELINING_SETUP_OPTS } from '../fixtures/fixtures.js'; import { CrossChainMessagingTest } from './cross_chain_messaging_test.js'; +// Public L1→L2 token deposit and L2→L1 withdrawal via the TokenBridge. Uses CrossChainMessagingTest +// with startProverNode=true (prod sequencer, pipelining preset: ethSlot=4s, aztecSlot=12s), fake +// in-proc prover node, and CrossChainTestHarness for full L1↔L2 portal/bridge lifecycle. Setup and +// teardown happen per-test (beforeEach/afterEach) because the test creates fresh bridge state each run. describe('e2e_cross_chain_messaging token_bridge_public', () => { // Pipelining slows wall-clock chain progress (12s slots); waitForProven via advanceToEpochProven // needs more than the default 300s per-test budget. @@ -39,6 +43,9 @@ describe('e2e_cross_chain_messaging token_bridge_public', () => { await t.teardown(); }); + // Full round-trip: mint on L1, publicly deposit via TokenPortal, wait for message, claim_public on + // L2, authorize bridge to burn, withdraw to L1, advance to epoch proven, consume Outbox on L1. + // Asserts L1 balance is restored after the round-trip. it('Publicly deposit funds from L1 -> L2 and withdraw back to L1', async () => { const l1TokenBalance = 1000000n; const bridgeAmount = 100n; @@ -94,6 +101,8 @@ describe('e2e_cross_chain_messaging token_bridge_public', () => { // Advance the epoch until the tx is proven since the messages are inserted to the outbox when the epoch is proven. await t.advanceToEpochProven(l2TxReceipt); + // REFACTOR: hand-rolled retryUntil polling for L2→L1 membership witness; replace with a + // waitForL2ToL1MessageWitness(node, txHash, leaf) helper shared across bridge tests. const l2ToL1MessageResult = await retryUntil( () => aztecNode.getL2ToL1MembershipWitness(l2TxReceipt.txHash, l2ToL1Message), 'l2 to l1 membership witness', @@ -113,6 +122,8 @@ describe('e2e_cross_chain_messaging token_bridge_public', () => { expect(await crossChainTestHarness.getL1BalanceOf(ethAccount)).toBe(l1TokenBalance - bridgeAmount + withdrawAmount); }, 900_000); + // User2 tries to claim to their own address (fails), then correctly claims to ownerAddress. + // Asserts only ownerAddress receives the tokens, user2 gets nothing, and the message is consumed. it('Someone else can mint funds to me on my behalf (publicly)', async () => { const l1TokenBalance = 1000000n; const bridgeAmount = 100n; diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index d9b33e2019ab..bed7cffbe82b 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -17,7 +17,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; jest.setTimeout(400_000); -// Tests crowdfunding via the Crowdfunding contract and claiming the reward token via the Claim contract +// Tests crowdfunding via the Crowdfunding contract and claiming the reward token via the Claim contract. +// Uses setup(3, AUTOMINE_E2E_OPTS) with one node, automine sequencer, three accounts (operator, 2 donors). +// One test warps L1 time via cheatCodes.eth.warp to pass the deadline. jest.setTimeout(400s). describe('e2e_crowdfunding_and_claim', () => { const donationTokenMetadata = { name: 'Donation Token', @@ -129,6 +131,8 @@ describe('e2e_crowdfunding_and_claim', () => { await teardown(); }); + // Happy path: donor1 donates via authwit, claims reward token via Claim contract, operator + // withdraws. Asserts DNT and RWT balances match expected values throughout. it('full donor flow', async () => { const donationAmount = 1000n; @@ -182,11 +186,13 @@ describe('e2e_crowdfunding_and_claim', () => { expect(balanceDNTAfterWithdrawal).toEqual(donationAmount); }); + // Attempts to claim the same UintNote that was consumed in the previous test; expects a revert. it('cannot claim twice', async () => { // The first claim was executed in the previous test await expect(claimContract.methods.claim(uintNote, donor1Address).send({ from: donor1Address })).rejects.toThrow(); }); + // donor2 donates, then donor1 tries to claim donor2's note; expects an owner check failure. it('cannot claim with a different address than the one that donated', async () => { const donationAmount = 1000n; @@ -220,6 +226,7 @@ describe('e2e_crowdfunding_and_claim', () => { // docs:end:local-tx-fails }); + // Modifies an existing note's randomness to make it non-existent, then tries to claim; expects revert. it('cannot claim with a non-existent note', async () => { // We get a non-existent note by copy the UintNote and change the randomness to a random value const nonExistentNote = { ...uintNote }; @@ -230,6 +237,8 @@ describe('e2e_crowdfunding_and_claim', () => { ).rejects.toThrow(); }); + // Deploys a second Crowdfunding instance, donor1 donates to it, then attempts to claim that note + // via the original Claim contract (which only accepts notes from the original Crowdfunding). Expects revert. it('cannot claim with existing note which was not emitted by a different contract', async () => { // 1) Deploy another instance of the crowdfunding contract let otherCrowdfundingContract: CrowdfundingContract; @@ -280,6 +289,7 @@ describe('e2e_crowdfunding_and_claim', () => { ).rejects.toThrow(); }); + // donor2 donates, then tries to withdraw from the operator's position; expects "Not an operator" revert. it('cannot withdraw as a non-operator', async () => { const donationAmount = 500n; @@ -304,6 +314,7 @@ describe('e2e_crowdfunding_and_claim', () => { ).rejects.toThrow('Assertion failed: Not an operator'); }); + // Warps L1 time past the deadline via cheatCodes.eth.warp, then attempts to donate; expects revert. it('cannot donate after a deadline', async () => { const donationAmount = 1000n; diff --git a/yarn-project/end-to-end/src/e2e_custom_message.test.ts b/yarn-project/end-to-end/src/e2e_custom_message.test.ts index 464894af2ee3..19b982f08856 100644 --- a/yarn-project/end-to-end/src/e2e_custom_message.test.ts +++ b/yarn-project/end-to-end/src/e2e_custom_message.test.ts @@ -12,6 +12,9 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; +// Tests the CustomMessage contract's multi-log event pattern: emitting a single event split across +// multiple private logs and reassembling it via wallet.getPrivateEvents. +// Uses setup(1, AUTOMINE_E2E_OPTS) with one node, automine sequencer, one account. describe('CustomMessage - Multi-Log Pattern', () => { let contract: CustomMessageContract; jest.setTimeout(TIMEOUT); @@ -31,6 +34,8 @@ describe('CustomMessage - Multi-Log Pattern', () => { afterAll(() => teardown()); + // Emits one MultiLogEvent via emit_multi_log_event, retrieves it via getPrivateEvents, and + // asserts all four field values match. it('reassembles a multi-log event from multiple private logs', async () => { const values = [Fr.random(), Fr.random(), Fr.random(), Fr.random()]; @@ -52,6 +57,8 @@ describe('CustomMessage - Multi-Log Pattern', () => { expect(events[0].event.value3).toBe(values[3].toBigInt()); }); + // Emits two MultiLogEvents in a single BatchCall, retrieves both, and asserts all eight field + // values match by matching on value0. it('reassembles multiple multi-log events from the same transaction', async () => { const valuesA = [Fr.random(), Fr.random(), Fr.random(), Fr.random()]; const valuesB = [Fr.random(), Fr.random(), Fr.random(), Fr.random()]; diff --git a/yarn-project/end-to-end/src/e2e_debug_trace.test.ts b/yarn-project/end-to-end/src/e2e_debug_trace.test.ts index fbb3282245a2..1df4c0fde525 100644 --- a/yarn-project/end-to-end/src/e2e_debug_trace.test.ts +++ b/yarn-project/end-to-end/src/e2e_debug_trace.test.ts @@ -18,6 +18,12 @@ import { type Hex, decodeFunctionData, encodeFunctionData, multicall3Abi } from import { getPrivateKeyFromIndex, setup } from './fixtures/utils.js'; +// Tests that the sequencer can successfully process blocks when L1 block proposals are forwarded +// via a proxy contract (Forwarder). Also tests that a corrupted first propose call (failing with +// allowFailure:true) followed by a valid second call still produces blocks. +// Uses setup(2, {ethereumSlotDuration:4, aztecSlotDuration:12, proofSubEpochs:640, minTxsPerBlock:0, +// aztecEpochDuration=default}) — production sequencer, anvil interval mining. The L1 interaction is +// Forwarder/Multicall3/Rollup contract interception for block-proposal routing, not cross-chain bridging. describe('e2e_debug_trace_transaction', () => { jest.setTimeout(5 * 60 * 1000); // 5 minutes @@ -81,6 +87,8 @@ describe('e2e_debug_trace_transaction', () => { afterAll(() => teardown()); // In this test we deploy a simple forwarder contract to L1, this serves as an additional proxy + // Intercepts sendAndMonitorTransaction to forward the Multicall3 call via the Forwarder proxy. + // Waits for 2 new blocks via retryUntil; asserts the chain advances. it('can process blocks using debug trace', async () => { // We intercept calls to sendAndMonitorTransaction to forward inner calls via the forwarder const l1Utils: L1TxUtils[] = (publisherManager as any).publishers; @@ -129,6 +137,7 @@ describe('e2e_debug_trace_transaction', () => { const numBlocksToMine = 2; const startBlockNumber = await aztecNode.getBlockNumber(); await aztecNodeAdmin.setConfig({ minTxsPerBlock: 0 }); + // REFACTOR: raw retryUntil poll on block number; replace with a waitForBlock(n) DSL helper const result = await retryUntil( async () => { const blockNumber = await aztecNode.getBlockNumber(); @@ -144,6 +153,9 @@ describe('e2e_debug_trace_transaction', () => { l1Utils[0].sendAndMonitorTransaction = originalSendAndMonitor; }); + // Intercepts Multicall3 aggregate3, prepends a corrupted call (coinbase zeroed, allowFailure=true) + // before the original calls. Waits for 3 new blocks; asserts the chain advances despite the + // first inner call reverting. it('can process blocks with a failing call followed by a successful call', async () => { // We intercept calls to sendAndMonitorTransaction to: // 1. Decode the Multicall3 aggregate3 call @@ -247,6 +259,7 @@ describe('e2e_debug_trace_transaction', () => { const numBlocksToMine = 3; const startBlockNumber = await aztecNode.getBlockNumber(); await aztecNodeAdmin.setConfig({ minTxsPerBlock: 0 }); + // REFACTOR: raw retryUntil poll on block number; replace with a waitForBlock(n) DSL helper const result = await retryUntil( async () => { const blockNumber = await aztecNode.getBlockNumber(); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts index 86b465607e15..b5d637a3219c 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/contract_class_registration.test.ts @@ -24,6 +24,10 @@ import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from '../fixtures/fixtures.js'; import { DeployTest, type StatefulContractCtorArgs } from './deploy_test.js'; +// Tests low-level contract class and instance registration: publishing class bytecode, deploying +// instances via wallet or a contract deployer, and init-check enforcement. DeployTest wraps +// setup(0, { ...AUTOMINE_E2E_OPTS, fundSponsoredFPC, skipAccountDeployment }) with 1 account. +// jest.setTimeout is 900s because serial publish/deploy chains exceed the default 5 min hook budget. describe('e2e_deploy_contract contract class registration', () => { // Pipelined cadence (~24s/dependent-tx) inflates the chained deploy/publish setup beyond the default 5 min // hook window. Many of the publishInstance helpers serially register multiple contracts/instances per case. 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 c194d6677265..fb52792edfbd 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 @@ -15,6 +15,10 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { TestWallet } from '../test-wallet/test_wallet.js'; import { DeployTest } from './deploy_test.js'; +// Tests the high-level DeployMethod API: deploying contracts publicly, privately, with +// batching, and verifying deployment metadata. DeployTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, +// fundSponsoredFPC, skipAccountDeployment }) with 1 account. Includes a minTxsPerBlock=2 sub-test +// that verifies two txs land in the same block. describe('e2e_deploy_contract deploy method', () => { const t = new DeployTest('deploy method'); 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 17998c08d374..19f5492bbbb3 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 @@ -13,6 +13,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; import { DeployTest } from './deploy_test.js'; +// Tests legacy ContractDeployer API: basic deploy, consecutive rollups, duplicate-salt rejection, +// and failed public constructor handling. DeployTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, +// fundSponsoredFPC, skipAccountDeployment }) with 1 account. describe('e2e_deploy_contract legacy', () => { const t = new DeployTest('legacy'); 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 9fbbbbd36598..ee96b42f6194 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 @@ -17,6 +17,10 @@ import { DeployTest } from './deploy_test.js'; type InitTestCtorArgs = Parameters; +// Tests private contract initialization flows: noinitcheck functions, contracts without constructors, +// single/batch initialization, ordering constraints between private init and public calls, and +// ContractInitializationStatus reporting. DeployTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, +// fundSponsoredFPC, skipAccountDeployment }) with 1 account. describe('e2e_deploy_contract private initialization', () => { const t = new DeployTest('private initialization'); diff --git a/yarn-project/end-to-end/src/e2e_double_spend.test.ts b/yarn-project/end-to-end/src/e2e_double_spend.test.ts index 94bb2ec72f7d..ae02243d2c10 100644 --- a/yarn-project/end-to-end/src/e2e_double_spend.test.ts +++ b/yarn-project/end-to-end/src/e2e_double_spend.test.ts @@ -8,6 +8,8 @@ import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Tests that a public nullifier emitted in one tx cannot be emitted again in a subsequent tx. +// Uses setup(1, AUTOMINE_E2E_OPTS) with one node, automine sequencer, one funded account. describe('e2e_double_spend', () => { let wallet: Wallet; let defaultAccountAddress: AztecAddress; @@ -33,7 +35,10 @@ describe('e2e_double_spend', () => { afterAll(() => teardown()); + // Verifies the public nullifier duplicate rejection path: simulation fails, then direct send reverts. describe('double spends', () => { + // Emits nullifier=1 publicly, then simulates the same — expects "duplicate nullifier" error. + // Then sends without simulation and expects REVERTED status. it('emits a public nullifier and then tries to emit the same nullifier', async () => { const nullifier = new Fr(1); await contract.methods.emit_nullifier_public(nullifier).send({ from: defaultAccountAddress }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_empty_blocks_proof.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_empty_blocks_proof.test.ts index af638e93be69..00d4d5fe688e 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_empty_blocks_proof.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_empty_blocks_proof.test.ts @@ -10,6 +10,10 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 15); +// Single-node epoch suite (default EpochsTestContext, no extra validator nodes). Starts a prover +// node (fake proofs). Sets minTxsPerBlock=1 after setup so blocks are empty, then verifies that +// the prover still submits a proof for those empty-block checkpoints within the proof submission +// window. describe('e2e_epochs/epochs_empty_blocks_proof', () => { let context: EndToEndContext; let rollup: RollupContract; @@ -30,10 +34,15 @@ describe('e2e_epochs/epochs_empty_blocks_proof', () => { await test.teardown(); }); + // Raises minTxsPerBlock to 1 so the sequencer cannot build blocks, advances to epoch 1, + // then waits for the prover to submit a proof for the empty checkpoint. Asserts that the + // monitor's checkpointNumber matches the proven target, confirming the proof landed on L1. it('submits proof even if there are no txs to build a block', async () => { context.sequencer?.updateConfig({ minTxsPerBlock: 1 }); await test.waitUntilEpochStarts(1); + // REFACTOR: raw sleep to flush pending L1 txs; replace with a helper that waits for the + // sequencer to finish all in-flight L1 publishes (e.g. waitForSequencerIdle). // Sleep to make sure any pending checkpoints are published await sleep(L1_BLOCK_TIME_IN_S * 1000); const checkpointNumberAtEndOfEpoch0 = await rollup.getCheckpointNumber(); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_equivocation.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_equivocation.test.ts index 1a45dfe19118..485ec595b03a 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_equivocation.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_equivocation.test.ts @@ -34,6 +34,11 @@ const NODE_COUNT = 4; * * The test verifies that L1 sync overrides the gossip-only proposal on all observer * nodes (B, C, D) once A's L1-confirmed checkpoint propagates via the archiver. + * + * It additionally verifies that the chain heals after node A is stopped, and that every observing + * validator records a DUPLICATE_PROPOSAL slashing offense. + * + * Uses EpochsTestContext with mockGossipSubNetwork, no initial sequencer, and slasherEnabled. */ describe('e2e_epochs/epochs_equivocation', () => { let logger: Logger; @@ -45,6 +50,10 @@ describe('e2e_epochs/epochs_equivocation', () => { await test?.teardown(); }); + // Creates 4 nodes (A holds all keys, B/C each hold 2, D is an observer). Warps L1 to one slot + // before the target slot so pipelining engages. Waits for B/C/D to see the gossip-only proposal + // then for A's L1-confirmed checkpoint to override it on those nodes. Stops A, re-enables + // publishing on B/C, waits for chain recovery, and asserts DUPLICATE_PROPOSAL offense on B and C. it('L1-confirmed checkpoint overrides gossip-only equivocating proposal', async () => { // Build 4 validators (V1..V4) using getPrivateKeyFromIndex(i+3), same convention as other epoch tests. const validators = times(NODE_COUNT, i => { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_first_slot.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_first_slot.test.ts index 6d4acc9d08a5..e820d2f05d63 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_first_slot.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_first_slot.test.ts @@ -31,10 +31,12 @@ const NODE_COUNT = 8; const COMMITTEE_SIZE = 3; const TX_COUNT = 8; -// Spawns NODE_COUNT validator nodes, connected via a mocked gossip sub network, but sets -// committee size to 3. Warps to immediately before the beginning of an epoch, and checks -// that the first slot of the epoch is mined without any errors. -// Regression test for https://github.com/AztecProtocol/aztec-packages/issues/15414 +// Regression test for https://github.com/AztecProtocol/aztec-packages/issues/15414. +// Eight validator nodes share a mocked gossip bus with a committee size of 3. Sends 8 txs +// (one per sub-slot, maxTxsPerBlock=1), then warps L1 to just before an epoch boundary so +// the pipelined proposer's first build window targets the epoch's first slot. Verifies that +// blocks are built on both the first and second slots of the new epoch. +// Uses EpochsTestContext with mockGossipSubNetwork, no initial sequencer, no prover node. describe('e2e_epochs/epochs_first_slot', () => { let context: EndToEndContext; let logger: Logger; @@ -96,6 +98,9 @@ describe('e2e_epochs/epochs_first_slot', () => { await test.teardown(); }); + // Pre-proves 8 txs and sends them without waiting. Warps to one L1 block before the first slot + // of an epoch that is two epochs ahead. Starts all sequencers and waits for all txs to be mined. + // Asserts that blocks with the epoch's first and second slot numbers are present in the archiver. it('builds blocks on the first two slots of the epoch', async () => { // Create and submit txs for the first two slots of the epoch // We set maxTxsPerBlock to 1, so two txs mean two consecutive blocks @@ -138,6 +143,8 @@ describe('e2e_epochs/epochs_first_slot', () => { const [firstSlot] = getSlotRangeForEpoch(epoch, test.constants); const secondSlot = SlotNumber(firstSlot + 1); logger.warn(`Waiting until blocks are synced for slots ${firstSlot} and ${secondSlot}`); + // REFACTOR: hand-rolled poll checking block slots; replace with a helper such as + // waitUntilBlocksForSlots(nodes[0], [firstSlot, secondSlot], timeout). await retryUntil( async () => { const blocks = await nodes[0].getBlocks(BlockNumber(INITIAL_L2_BLOCK_NUM), 10); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_checkpoint_handoff.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_checkpoint_handoff.test.ts index 6f2d1c4e31c6..ded9c6c5808c 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_checkpoint_handoff.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_checkpoint_handoff.test.ts @@ -54,6 +54,22 @@ const VALIDATOR_COUNT = 4; * checkpoint it prunes the S1 block as an orphan, rebuilds checkpoint 1 itself, and never produces S2's * checkpoint. With the fix, checkpoint 1 (covering S1, built by the builder) and checkpoint 2 (covering * S2, built by the peer) both land on L1, and S2's covered block carries the peer's distinct coinbase. + * + * Setup: `EpochsTestContext.setup` with 4 validators (`skipInitialSequencer: true`) wired onto the in-memory + * `mockGossipSubNetwork` bus, then 4 validator nodes created via `test.createValidatorNode` in 2 HA pairs. Each pair + * shares its two validator keys plus an in-memory `createSharedSlashingProtectionDb` (so only one peer signs per duty) + * — explicitly NOT the Postgres-backed docker-compose HA suite, so this is an in-proc `multi-node` test, not infra. + * Production `Sequencer`, no prover node. Timing: ethSlot=6s, aztecSlot=36s, epoch=8, proofSubEpochs=1024, + * blockDurationMs=8s, committeeSize=4, attestationPropagationTime=0.5, inboxLag=2; anvil on interval mining. Nodes build + * empty checkpoints (`buildCheckpointIfEmpty` + `minTxsPerBlock: 0`) so no txs are needed, and each node uses a distinct + * coinbase so the secondary assertion can prove which peer produced S2. Time is warped with `cheatCodes.eth.warp`: + * `findConsecutiveSamePairSlots` recovers from `ValidatorSelection__EpochNotStable` by warping forward one epoch, and + * the test warps to one L1 slot before S1's build slot before starting the sequencers. Routing of S1→builder and + * S2→peer uses the test-only `pauseProposingForSlots` hook. + * + * Proposed category: `multi-node` (epochs/) — 4 validators on the mock gossip bus (mirrors + * `epochs_orphan_block_prune` / `epochs_simple_block_building`). See the inline REFACTOR markers for hand-rolled + * coordination a DSL helper should replace. */ describe('e2e_epochs/epochs_ha_checkpoint_handoff', () => { let context: EndToEndContext; @@ -192,6 +208,9 @@ describe('e2e_epochs/epochs_ha_checkpoint_handoff', () => { ? undefined : haPairs.find(pair => pair.addresses.includes(proposer.toString().toLowerCase())); + // REFACTOR: hand-rolled slot-search loop with manual epoch arithmetic and warp-on-EpochNotStable retry + // (same pattern as epochs_invalidate_block / epochs_orphan_block_prune) — a shared "find slots matching a + // proposer predicate, warping past EpochNotStable" helper should replace it. let candidate = Number(test.epochCache.getEpochAndSlotNow().slot) + 4; const maxAttempts = 200; for (let attempt = 0; attempt < maxAttempts; attempt++) { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_sync.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_sync.test.ts index f4a2ad646cb9..923ac1b9bff0 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_sync.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_ha_sync.test.ts @@ -32,6 +32,12 @@ const TX_COUNT = 6; * E2E test for HA (High Availability) proposed chain sync. * Verifies that nodes sharing validator keys with the proposer still process * block proposals and sync to the proposed chain, rather than ignoring them. + * + * Creates two HA pairs (nodes sharing validator keys) with a shared SlashingProtectionDatabase per + * pair, and disables checkpoint publishing on all validator nodes so every node — including the HA + * peer that did NOT build a given block — must sync to the proposed chain tip via P2P before any + * checkpoint lands on L1. Uses EpochsTestContext with mockGossipSubNetwork and pxeOpts + * syncChainTip='proposed'. */ describe('e2e_epochs/epochs_ha_sync', () => { let context: EndToEndContext; @@ -131,6 +137,10 @@ describe('e2e_epochs/epochs_ha_sync', () => { await test?.teardown(); }); + // Sends 6 txs, warps to one L1 slot before the next L2 slot, and starts all four sequencers. + // Waits until every node has a proposed tip strictly above the checkpointed tip, confirming + // that blocks arrived via P2P proposals (not from L1 checkpoints). Checks all four nodes agree + // on the block hash at the minimum proposed tip. Asserts no new checkpoints were published. it('HA peers sync to proposed chain from proposals signed by their own validator keys', async () => { await setupTest(); @@ -165,6 +175,8 @@ describe('e2e_epochs/epochs_ha_sync', () => { // Wait until all nodes have proposed blocks strictly beyond the checkpointed tip. // This ensures we're checking blocks produced by validators via P2P proposals, // not blocks synced from L1 checkpoints during setup. + // REFACTOR: hand-rolled poll over all archivers checking proposed > checkpointed; replace with + // a test-context helper such as waitUntilAllNodesProposedBeyondCheckpointed(nodes, timeout). await retryUntil( async () => { const tips = await Promise.all(allArchivers.map(a => a.getL2Tips())); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_high_tps_block_building.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_high_tps_block_building.test.ts index c6ece3776d92..4dbd40134084 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_high_tps_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_high_tps_block_building.test.ts @@ -30,8 +30,9 @@ const NODE_COUNT = 3; // checkpoint tx lands on the 2nd L1 block of its target slot. // // Config: aztecSlotDuration=36s, ethereumSlotDuration=12s (3 L1 blocks / L2 slot), blockDuration=6s, -// fakeProcessingDelayPerTxMs=2500ms, attestationPropagationTime=1s, l1PublishingTime=12s, -// txDelayerMaxInclusionTimeIntoSlot=1s. +// fakeProcessingDelayPerTxMs=2500ms, attestationPropagationTime=1s, +// txDelayerMaxInclusionTimeIntoSlot=1s. (v5: the explicit l1PublishingTime override was dropped — +// EpochsTestContext no longer takes it; the publish window is now the framework default.) // // Time inside a build slot (36s total): // T=0-1 (1s) init (checkpointInitializationTime) @@ -67,6 +68,11 @@ const BLOCK_DURATION_MS = 6000; const L2_SLOT_DURATION_S = 36; const L1_BLOCK_TIME_S = 12; +// Multi-block-per-slot suite verifying that 3 validator nodes can build fully-filled checkpoints +// (4 blocks × 2 txs each) under proposer pipelining with fake tx processing delays. Asserts that +// CHECKPOINTS_TO_CHECK consecutive checkpoints at or after the target slot each have at least +// BLOCKS_PER_CHECKPOINT-1 blocks and that the checkpoint tx lands in the 1st or 2nd L1 block of the +// target slot. Uses EpochsTestContext with mockGossipSubNetwork, no initial sequencer, no prover node. describe('e2e_epochs/epochs_high_tps_block_building', () => { let context: EndToEndContext; let logger: Logger; @@ -123,6 +129,10 @@ describe('e2e_epochs/epochs_high_tps_block_building', () => { await test.teardown(); }); + // Pre-proves TX_COUNT txs and sends them all, then sleeps until the target slot's pipelining + // build window is reachable. Starts all sequencers and waits for all txs to be mined. Groups + // blocks by checkpoint number and for each checkpoint at or after the target slot asserts block + // count, per-block tx count, and L1 submission offset. Expects zero fail events. it('builds blocks without any errors', async () => { // Pre-prove and send all txs so the proposer has a full backlog ready in the pool when it starts building. const txs = await timesAsync(TX_COUNT, i => @@ -153,6 +163,8 @@ describe('e2e_epochs/epochs_high_tps_block_building', () => { logger.warn( `Waiting until ${startSequencersAt.toISOString()} (${leadSeconds}s before L2 slot ${targetSlot} starts)`, ); + // REFACTOR: manual slot-timing calculation followed by sleepUntil; replace with a helper + // such as test.waitUntilBuildWindowForSlot(targetSlot) that encapsulates lead-time arithmetic. await sleepUntil(startSequencersAt, context.dateProvider.nowAsDate()); await Promise.all(sequencers.map(sequencer => sequencer.start())); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts index b54f869d1296..939ecc2fe1aa 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.parallel.test.ts @@ -40,6 +40,12 @@ const VALIDATOR_COUNT = 6; const BASE_ANVIL_PORT = getAnvilPort(); +// Six-validator suite (one key per node) exercising checkpoint invalidation paths. All nodes use +// a mocked gossip bus. The setup injects bad configs (insufficient attestations, fake/high-s/ +// unrecoverable signatures, shuffled attestations, parent-validity bypasses) to force invalid +// checkpoints, then verifies the next good proposer invalidates them and the chain progresses. +// Slasher is enabled. Uses EpochsTestContext with mockGossipSubNetwork, no initial sequencer, no +// prover node; ports are port-bumped per test via anvilPortOffset to support parallel execution. describe('e2e_epochs/epochs_invalidate_block', () => { let context: EndToEndContext; let logger: Logger; @@ -256,6 +262,11 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); } + // Configures all sequencers to skip attestation collection and sets minBlocksForCheckpoint=2. + // Sends 2 txs, waits for the first bad checkpoint to land (insufficient attestations), then + // lets a slot pass, sends more txs, and waits for a good proposer to invalidate the bad + // checkpoint in the same L1 tx as a new valid checkpoint. Verifies PROPOSED_INSUFFICIENT_ATTESTATIONS + // offense is recorded and chain progresses to checkpoint+2. // To be able to post its own checkpoint under pipelining, there should be no "proposed" checkpoint in flight, // otherwise we consider it's the proposed checkpoint that will invalidate the previous one. If it's the // proposed checkpoint the one that ends up being invalid, we need to discard our work, and cannot post our own. @@ -360,6 +371,10 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); + // Starts sequencers with good config, waits for the first checkpoint, then searches for two + // consecutive bad slots with distinct proposers. Applies skipCollectingAttestations to both bad + // proposers and waits for both bad checkpoints to land. Asserts the earliest is invalidated by + // the next good proposer, restores good config, and verifies the chain can produce checkpoint+3. // Here we disable invalidation checks from two of the proposers. Our goal is to get two invalid checkpoints // in a row, so the third proposer invalidates the earliest one, and the chain progresses. Note that the // second invalid checkpoint will also have invalid attestations, we are *not* testing the scenario where the @@ -533,6 +548,12 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); + // Regression for archiver infinite-loop on P1 (insufficient attestations) + P2 (valid descendant + // of P1 but bypasses the parent-validity gate). Warps L1 to the build window, starts sequencers, + // waits for both P1 and P2 checkpoints to land on L1. Asserts the chain advances past P2 (archiver + // no longer stalls), the DescendentOfInvalidAttestationsCheckpointDetected event fires, and both + // P1/P2 proposers are flagged for slashing (PROPOSED_INSUFFICIENT_ATTESTATIONS and + // PROPOSED_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS respectively). // P1 publishes a checkpoint with insufficient attestations; the next proposer P2 publishes a // valid descendant without first invalidating P1. Before the fix, the archiver tripped its // `InitialCheckpointNumberNotSequentialError` consecutive-number guard, rolled back the L1 @@ -554,6 +575,9 @@ describe('e2e_epochs/epochs_invalidate_block', () => { minTxsPerBlock: 0, }), ); + // REFACTOR: hand-rolled slot-search with EpochNotStable warp fallback; replace with a shared + // helper such as findNextTwoSlotsWithDistinctProposers(test, fromSlot) that encapsulates the + // EpochNotStable retry-and-warp loop. let badSlot1: SlotNumber | undefined; let p1Proposer: EthAddress | undefined; let p2Proposer: EthAddress | undefined; @@ -740,6 +764,10 @@ describe('e2e_epochs/epochs_invalidate_block', () => { ); }); + // All sequencers skip attestation collection and invalidation-as-proposer. Waits for the first + // bad checkpoint to land, then waits for a committee member to trigger invalidation after the + // configured delay. Asserts the invalidation happened at or after the slot's timestamp plus the + // committee invalidation delay. // All tests but this one disable invalidation by committee. This test disables invalidation by proposer and // instead waits for a committee member to invalidate the block after several proposers not doing so. it('committee member invalidates a block if proposer does not come through', async () => { @@ -790,6 +818,10 @@ describe('e2e_epochs/epochs_invalidate_block', () => { logger.warn(`Test succeeded '${expect.getState().currentTestName}'`); }); + // All sequencers use skipCollectingAttestations. After the second CheckpointInvalidated event + // (same checkpoint number invalidated twice), re-enables attestation collection and verifies the + // chain produces checkpoint+2. Guards against the regression where the invalidator used the + // wrong checkpoint when the re-invalidated checkpoint number changed. // Regression for an issue where, if the invalidator proposed another invalid checkpoint, the next proposer would // try invalidating the first one, which would fail due to mismatching attestations. For example: // Slot S: Checkpoint N is proposed with invalid attestations @@ -802,6 +834,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); }); + // Same double-invalidation scenario as above but using injectFakeAttestation instead of + // skipCollectingAttestations. Regression for a London Q4-2025 attack vector. // Regression for Joe's Q42025 London attack. Same as above but with an invalid signature instead of insufficient ones. it('chain progresses if a checkpoint with an invalid attestation is invalidated with an invalid one', async () => { await runDoubleInvalidationTest({ @@ -810,6 +844,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); }); + // Injects a high-s ECDSA signature (rejected by L1 OpenZeppelin ECDSA but valid offchain), + // waits for the resulting bad checkpoint, then verifies a good proposer invalidates it. // Regression for A-71: Ensure the node correctly invalidates checkpoints where an attestation has a malleable // signature (high-s value). The Rollup contract uses OpenZeppelin's ECDSA recover which rejects high-s values // per EIP-2, so these signatures recover to address(0) on L1 but may succeed offchain. @@ -820,6 +856,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); }); + // Injects an unrecoverable signature (e.g. r=0; ecrecover returns address(0) on L1). + // Waits for the bad checkpoint then verifies a good proposer invalidates it. Regression for A-71. // Regression for A-71: Ensure the node correctly invalidates checkpoints where an attestation's signature // cannot be recovered (e.g. r=0). On L1, ecrecover returns address(0) for such signatures. it('proposer invalidates checkpoint with unrecoverable signature attestation', async () => { @@ -829,6 +867,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { }); }); + // Injects shuffled attestation ordering (accepted offchain but rejected by L1 which requires the + // committee order). Waits for the bad checkpoint then verifies a good proposer invalidates it. // Regression for the node accepting attestations that did not conform to the committee order, // but L1 requires the same ordering. See #18219. it('proposer invalidates previous block with shuffled attestations', async () => { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts index b3566028d19f..c0f3c2dcfcf2 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts @@ -31,6 +31,11 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 20); +// Single-node + prover-node suite exercising L1 reorg behavior for both block data and L1→L2 +// messages. Uses EthCheatCodes reorg/reorgWithReplacement to remove or insert L1 transactions +// and verifies the archiver and node prune/restore their views accordingly. Prover and sequencer +// delayers intercept L1 txs to enable controlled reorg scenarios. Uses EpochsTestContext defaults +// (single initial sequencer, fake prover, no mock gossip); actively drives L1 via cheatcodes. describe('e2e_epochs/epochs_l1_reorgs', () => { let context: EndToEndContext; let logger: Logger; @@ -90,6 +95,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { await test.teardown(); }); + // Suite covering L1 reorg effects on L2 block state: proof removal, proof re-addition via + // reorg, checkpoint removal from pending chain, and checkpoint insertion via reorg. describe('blocks', () => { const getBlobs = async (serializedTx: `0x${string}`) => { const parsedTx = parseTransaction(serializedTx); @@ -107,6 +114,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { const getProvenCheckpointNumber = (node: AztecNode) => node.getChainTips().then(tips => tips.proven.checkpoint.number); + // Waits for an initial proof to land, stops the prover, reorgs L1 to remove the proof block, + // waits for the proof submission window to expire, spins up a new sync-only node, and verifies + // both the new node and the old node have rolled back to the pre-proof checkpoint number. it('prunes L2 blocks if a proof is removed due to an L1 reorg', async () => { /** Logs a full state snapshot: L1 latest/finalized and archiver L2 tips. */ const logState = async (label: string) => { @@ -197,6 +207,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // And check that the old node has processed the reorg as well logger.warn(`Testing old node after reorg`); + // REFACTOR: hand-rolled poll on proven checkpoint equality; replace with + // test.waitUntilProvenCheckpointNumber(initialProvenCheckpoint, timeout). await retryUntil( () => getProvenCheckpointNumber(node).then(cp => cp === initialProvenCheckpoint), 'prune', @@ -213,6 +225,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { await newNode.stop(); }); + // Waits for a proof, stops the prover, removes the proof via reorgWithReplacement (same block + // count), starts a fresh prover node, and verifies a new proof lands and the node re-syncs to + // the proven state without having pruned. it('does not prune if a second proof lands within the submission window after the first one is reorged out', async () => { // Send txs to trigger multi-block checkpoints await sendTransactions(TX_COUNT); @@ -260,6 +275,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { // New prover's aztec node is stopped in test.teardown() }); + // Cancels the next prover L1 tx so no proof lands, waits for the end of the submission window + // (triggering pruning), then reorgs L1 to include the previously-cancelled proof tx and + // verifies the node un-prunes and resumes from the proven state. it('restores L2 blocks if a proof is added due to an L1 reorg', async () => { // Send txs to trigger multi-block checkpoints await sendTransactions(TX_COUNT); @@ -337,6 +355,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { logger.warn(`Test succeeded`); }); + // Waits until CHECKPOINT_NUMBER is mined and node synced, stops the sequencer, reorgs L1 to + // remove that checkpoint's L1 block, and verifies the node rolls back to checkpoint-1. it('prunes blocks from pending chain removed from L1 due to an L1 reorg', async () => { // Send txs to trigger multi-block checkpoints await sendTransactions(TX_COUNT); @@ -373,6 +393,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { await retryUntil(() => getCheckpointNumber(node).then(b => b === expectedCheckpointNumber), 'node sync', 30, 0.1); }); + // Cancels the next sequencer L1 tx (blocking CHECKPOINT_NUMBER from landing), waits for + // several more L1 blocks to pass, then reorgs L1 to include the previously-cancelled checkpoint + // tx and manually sends the blobs to the filestore. Verifies the node sees the new block. it('sees new blocks added in an L1 reorg', async () => { // Send txs to trigger multi-block checkpoints await sendTransactions(TX_COUNT); @@ -442,6 +465,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { }); }); + // Suite covering L1 reorg effects on L1→L2 cross-chain messages: removal of a sent message + // and insertion of a previously-cancelled message. describe('messages', () => { let l1Client: ExtendedViemWalletClient; let l1ClientDelayer: Delayer; @@ -456,6 +481,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { { l1ContractAddresses: context.deployL1ContractsValues.l1ContractAddresses, l1Client }, ); + // Sends 3 L1→L2 messages, waits for the last to be seen, reorgs it out, sends a replacement + // message, and verifies the replacement becomes ready while the removed message is gone. it('updates L1 to L2 messages changed due to an L1 reorg', async () => { // Send L2 txs to trigger multi-block checkpoints and wait for them to land in a checkpoint await sendTransactions(TX_COUNT, 100); @@ -490,6 +517,9 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { await test.assertMultipleBlocksPerSlot(2); }); + // Sends a first message, cancels a second message's L1 tx via delayer, waits for the archiver + // to advance past the cancelled block, then reorgs to include the cancelled message. Sends a + // third message on top and verifies all three are eventually seen by the node. it('handles missed message inserted by an L1 reorg', async () => { // Send L2 txs to trigger multi-block checkpoints and wait for them to land in a checkpoint await sendTransactions(TX_COUNT, 200); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_long_proving_time.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_long_proving_time.test.ts index 0f8d966a1061..6c68e2bff650 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_long_proving_time.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_long_proving_time.test.ts @@ -10,6 +10,12 @@ jest.setTimeout(1000 * 60 * 15); const MAX_JOB_COUNT = 20; +// Single-node + prover-node suite verifying that a prover node whose proving time spans multiple +// epochs (proverTestDelayMs ≈ 3 epochs) still eventually submits valid proofs while proving several +// epochs concurrently (proverNodeMaxPendingJobs=20, proverBrokerMaxEpochsToKeepResultsFor=10) without +// the broker rejecting in-flight jobs as stale. (v5: previously capped at one job at a time with +// proverNodeMaxPendingJobs=1; now exercises concurrent multi-epoch proving.) Uses EpochsTestContext +// default setup (single sequencer, fake prover with delay, no mock gossip). describe('e2e_epochs/epochs_long_proving_time', () => { let logger: Logger; let monitor: ChainMonitor; @@ -44,6 +50,10 @@ describe('e2e_epochs/epochs_long_proving_time', () => { await test.teardown(); }); + // Polls the prover node's job queue until provenCheckpointNumber reaches targetProvenEpochs. + // Asserts that checkpointNumber advanced at least 3× the proven epoch count, confirming proving + // lagged behind block production. Asserts maxJobCount stays within MAX_JOB_COUNT (20), confirming + // the node may run multiple proving jobs in parallel up to the configured cap. it('generates proof over multiple epochs', async () => { const targetProvenEpochs = process.env.TARGET_PROVEN_EPOCHS ? parseInt(process.env.TARGET_PROVEN_EPOCHS) : 1; const targetProvenBlockNumber = targetProvenEpochs * test.epochDuration; @@ -51,6 +61,9 @@ describe('e2e_epochs/epochs_long_proving_time', () => { // Wait until we hit the target proven block number, and keep an eye on how many proving jobs are run in parallel. let maxJobCount = 0; + // REFACTOR: hand-rolled sleep loop polling provenCheckpointNumber; replace with + // test.waitUntilProvenCheckpointNumber(targetProvenBlockNumber, timeout) and check job count + // separately via a one-time snapshot rather than updating inside the loop. while (monitor.provenCheckpointNumber === undefined || monitor.provenCheckpointNumber < targetProvenBlockNumber) { const jobs = await test.proverNodes[0].getProverNode()!.getJobs(); if (jobs.length > maxJobCount) { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_manual_rollback.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_manual_rollback.test.ts index b8387bbc8526..3c2066bc08dd 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_manual_rollback.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_manual_rollback.test.ts @@ -11,6 +11,9 @@ import { EpochsTestContext, type EpochsTestOpts } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Single-node suite exercising the aztecNodeAdmin.rollbackTo() API. Default EpochsTestContext with +// a very long epoch (aztecEpochDuration=100) so there are no L2 reorgs, no finalized blocks, and +// the full pending chain is prunable. Actively drives L1 via cheatcodes (reorgTo to remove blocks). describe('e2e_epochs/manual_rollback', () => { let context: EndToEndContext; let logger: Logger; @@ -30,11 +33,15 @@ describe('e2e_epochs/manual_rollback', () => { await test.teardown(); }); + // Sub-suite for rolling back to a block that has not been finalized (epoch=100 → no finalization). describe('to unfinalized block', () => { beforeEach(async () => { await setup({ aztecEpochDuration: 100 }); // No L2 reorgs, no finalized blocks }); + // Waits for checkpoint 4, pauses node sync, reorgs L1 by 2 blocks, calls rollbackTo on the + // node, and asserts blockNumber equals the rolled-back value. Resumes sync and verifies the + // node re-syncs to the same block. it('manually rolls back', async () => { logger.info(`Starting manual rollback test to unfinalized block`); context.sequencer?.updateConfig({ minTxsPerBlock: 0 }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts index 00e0f90fd3d3..72660abcfc85 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts @@ -47,6 +47,12 @@ const TX_COUNT = 10; /** * E2E tests for Multiple Blocks Per Slot (MBPS) functionality. * Tests that the system correctly builds multiple blocks within a single slot/checkpoint. + * + * Four-validator suite under mock gossip with a prover node (fake proofs); PXE mode varies per test + * (checkpointed vs proposed). Exercises MBPS with: checkpointed-anchored txs, proposed-anchored txs, + * L2→L1 messages, L1→L2 messages, non-validator re-execution sync, cross-slot contract deploy+call, + * and prover proving MBPS checkpoints. Uses EpochsTestContext with mockGossipSubNetwork and no + * initial sequencer. */ describe('e2e_epochs/epochs_mbps', () => { let context: EndToEndContext; @@ -201,6 +207,8 @@ describe('e2e_epochs/epochs_mbps', () => { await test?.teardown(); }); + // Pre-proves and sends TX_COUNT txs, starts sequencers, waits for all txs to be mined, asserts a + // checkpoint with ≥EXPECTED_BLOCKS_PER_CHECKPOINT exists, then waits for that checkpoint to be proven. it('builds multiple blocks per slot with transactions anchored to checkpointed block', async () => { await setupTest({ syncChainTip: 'checkpointed', minTxsPerBlock: 1, maxTxsPerBlock: 2 }); @@ -231,6 +239,9 @@ describe('e2e_epochs/epochs_mbps', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Starts sequencers then sends txs one at a time, anchoring each to the proposed block containing + // the previous tx (PXE in 'proposed' mode). Verifies tx anchor block numbers are monotonically + // non-decreasing. Asserts ≥2 blocks per checkpoint and waits for the MBPS checkpoint to be proven. it('builds multiple blocks per slot with transactions anchored to proposed blocks', async () => { await setupTest({ syncChainTip: 'proposed', minTxsPerBlock: 1, maxTxsPerBlock: 1 }); @@ -269,6 +280,9 @@ describe('e2e_epochs/epochs_mbps', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Deploys a cross-chain TestContract, pre-proves TX_COUNT L2→L1 message txs, sends them all, waits + // for all to be mined, then asserts the total L2→L1 message count across all blocks ≥ TX_COUNT, + // a MBPS checkpoint exists, and that checkpoint is proven. it('builds multiple blocks per slot with L2 to L1 messages', async () => { await setupTest({ syncChainTip: 'proposed', minTxsPerBlock: 1, maxTxsPerBlock: 2 }); @@ -324,6 +338,9 @@ describe('e2e_epochs/epochs_mbps', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Seeds L1→L2 messages, sends filler txs to advance the chain so messages become ready, then + // pre-proves and sends consume txs. Verifies all consume txs are mined, a MBPS checkpoint exists, + // and that checkpoint is proven. it('builds multiple blocks per slot with L1 to L2 messages', async () => { // L1→L2 messages only become ready once the chain advances `inboxLag` checkpoints past where they // were inboxed, and a checkpoint only advances when a block is built in a new slot. With @@ -423,6 +440,10 @@ describe('e2e_epochs/epochs_mbps', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Creates an extra non-validator node with alwaysReexecuteBlockProposals=true, sends txs, and + // waits until that node has stored a multi-block proposed slot (≥2 blocks) beyond its checkpointed + // tip. Verifies block effects are valid, then starts a second sync-only node and confirms it + // syncs the multi-block slot from scratch. it('builds multiple blocks per slot and non-validators re-execute and sync multi-block slots', async () => { await setupTest({ syncChainTip: 'proposed', minTxsPerBlock: 1, maxTxsPerBlock: 1 }); @@ -509,6 +530,10 @@ describe('e2e_epochs/epochs_mbps', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Pre-proves a high-priority deploy tx and a low-priority call tx for the same contract. Waits + // until just before the next L2 slot boundary, sends deploy first (then call after 1s), and + // waits for both to be checkpointed. Asserts deploy block < call block and both belong to the + // same checkpoint. Waits for that checkpoint to be proven. it('deploys a contract and calls it in separate blocks within a slot', async () => { await setupTest({ syncChainTip: 'checkpointed', @@ -547,6 +572,8 @@ describe('e2e_epochs/epochs_mbps', () => { // Wait until one L1 slot before the start of the next L2 slot. // This ensures both txs land in the pending pool right before the proposer starts building. + // REFACTOR: manual slot-timing arithmetic and waitUntilL1Timestamp call; replace with a helper + // such as test.waitUntilBuildWindowForNextSlot() that encapsulates this pattern. // REFACTOR: This should go into a shared "waitUntilNextSlotStartsBuilding" utility const currentL1Block = await test.l1Client.getBlock({ blockTag: 'latest' }); const currentTimestamp = currentL1Block.timestamp; diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts index a8fa73888dac..2d3a78d39103 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.pipeline.parallel.test.ts @@ -40,6 +40,12 @@ const TX_COUNT = 34; * E2E tests for proposer pipelining with Multiple Blocks Per Slot (MBPS). * Verifies that the block proposer in slot N is the validator scheduled on L1 for slot N+1 * (the proposer view uses a +1 slot offset). + * + * Four-validator suite with a prover node (fake proofs) and 500ms mock gossip latency to simulate + * adverse network conditions. Two tests: (1) normal pipelining flow asserting build-vs-submission + * slot offsets and blob-fetch promotion; (2) a proposer skips its checkpoint publish, triggering an + * uncheckpointed-blocks prune followed by recovery. Uses EpochsTestContext with mockGossipSubNetwork + * and no initial sequencer. */ describe('e2e_epochs/epochs_mbps_pipeline', () => { let context: EndToEndContext; @@ -226,6 +232,11 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => { await test?.teardown(); }); + // Pre-proves TX_COUNT txs, starts sequencers, waits for all txs to be mined. Asserts a + // MBPS checkpoint with ≥EXPECTED_BLOCKS_PER_CHECKPOINT blocks. Asserts every block's header + // slot equals build-slot+1 (pipelining offset). Verifies node-0 fetches blobs (promotion + // disabled) while nodes 1-3 skip blob fetching (promotion enabled). Waits for the checkpoint + // to be proven. it('pipelining builds blocks using slot plus 1 proposer and proves them', async () => { await setupTest({ syncChainTip: 'checkpointed', minTxsPerBlock: 1, maxTxsPerBlock: 2 }); @@ -301,6 +312,10 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => { await waitForProvenCheckpoint(multiBlockCheckpoint); }); + // Establishes a baseline at checkpoint 1. Identifies the next proposer and disables its + // checkpoint publishing. Waits for the L2PruneUncheckpointed event on the archiver, then + // re-enables publishing. Waits for all txs to be mined, asserts a MBPS checkpoint exists, + // verifies the pipelining offset, and checks recovery blockNumber > baseline. it('prunes uncheckpointed blocks when proposer fails to deliver', async () => { await setupTest({ syncChainTip: 'checkpointed', minTxsPerBlock: 1, maxTxsPerBlock: 2 }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps_redistribution.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps_redistribution.test.ts index 5e3a763c8889..1e0065c58d23 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps_redistribution.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps_redistribution.test.ts @@ -49,6 +49,12 @@ const LATE_TX_COUNT = 7; /** Total txs pre-proved before the test begins. */ const TOTAL_TX_COUNT = EARLY_TX_COUNT + LATE_TX_COUNT; +// Four-validator MBPS suite verifying that the per-block gas budget redistribution mechanism allows +// late transactions to fill the last blocks of a checkpoint whose earlier blocks were light. Two tests: +// (1) standard redistribution — early blocks consume minimal budget, late txs all fit across the last +// blocks; (2) validators should NOT apply the proposer's fair-share multiplier during re-execution — +// nodes with different perBlockAllocationMultiplier values must still attest for each other's blocks. +// Uses EpochsTestContext with mockGossipSubNetwork, startProverNode, no initial sequencer. /** * Verifies that checkpoint budget redistribution lets a burst of late transactions fit into the last * blocks of a checkpoint when the earlier blocks were light. @@ -151,6 +157,10 @@ describe('e2e_epochs/epochs_mbps_redistribution', () => { await test?.teardown(); }); + // Pre-proves TOTAL_TX_COUNT txs. Warps to just before the next L2 slot. Sends the first early tx + // before starting sequencers so block-1 is not empty. Feeds remaining early txs one per sub-slot + // (waiting for each to be proposed), then dumps all late txs at once. Waits for all txs to be + // mined and verifies the late txs landed across the last two blocks (redistribution gave them budget). it('redistributes checkpoint budget so a late burst fits across the last two blocks', async () => { await setupTest(); @@ -266,6 +276,9 @@ describe('e2e_epochs/epochs_mbps_redistribution', () => { /** * Verifies that validators do NOT apply the proposer's fair-share multiplier when re-executing blocks. * + * Configures nodes 0/1 with perBlockAllocationMultiplier=10 and nodes 2/3 with the default (1.2), + * and keeps the mempool topped up with a background loop. + * * Two of the four validator nodes are configured with a very large `perBlockAllocationMultiplier` (10), * allowing their proposer to pack multiple txs into a single block. The other two keep the default * multiplier (1.2), which limits them to 1 tx per block given the tight `maxTxsPerCheckpoint`. @@ -336,6 +349,8 @@ describe('e2e_epochs/epochs_mbps_redistribution', () => { await sleep(1000); } }; + // REFACTOR: hand-rolled background sleep loop keeping the mempool above a threshold; replace + // with a shared test utility such as startMempoolFeeder(wallet, contract, from, minPending). void keepMempoolFull(); // Build a lookup from attester address to validator index for proposer identification. diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts index d7cfa7be8bd6..76c0ed86819f 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts @@ -49,6 +49,8 @@ const NODE_COUNT = 4; * - During slotTwo, the pipelined proposer for slotThree builds on top of the (now genesis) * checkpointed tip → `proposed` advances again. * - During slotThree, that pipelined work is published → `checkpointed` finally advances. + * + * Uses EpochsTestContext with mockGossipSubNetwork, no initial sequencer, and no prover node. */ describe('e2e_epochs/epochs_missed_l1_publish', () => { let logger: Logger; @@ -60,6 +62,11 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { await test?.teardown(); }); + // Searches for slotOne..slotThree with three distinct proposers (warp on EpochNotStable). Sets + // skipPublishingCheckpointsPercent=100 on proposerOne's node. Warps L1 to slotZero-1 L1 block. + // Subscribes to prune events on all nodes. Starts all sequencers and verifies: proposed tip + // reaches slotOne then slotTwo; all nodes emit L2PruneUncheckpointed at slotOne end; recovery + // produces a checkpointed block at slotThree. Sanity-checks no unexpected fail events. it('all nodes prune and recover when proposer fails to publish to L1', async () => { // Build 4 distinct validators (V1..V4). One key per node, no overlap. const validators = times(NODE_COUNT, i => { @@ -110,6 +117,9 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { // reverts with `ValidatorSelection__EpochNotStable`. We handle this by warping L1 forward // one epoch at a time and retrying — after each warp the previously-unstable epoch becomes // queryable, and we bump the candidate to keep the +4 slot margin from the new "now". + // REFACTOR: hand-rolled slot-search with EpochNotStable warp fallback looking for three + // consecutive distinct-proposer slots; replace with a shared helper such as + // findConsecutiveSlotsWithDistinctProposers(test, fromSlot, count) that encapsulates this pattern. let slotOne: SlotNumber | undefined; let proposerOne: EthAddress | undefined; let proposerTwo: EthAddress | undefined; @@ -227,6 +237,8 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { // (1) During slotZero: the pipelined proposer for slotOne broadcasts. Every node sees a proposed block at slotOne. logger.warn(`Waiting for proposed chain to reach slot ${slotOne} on all nodes (build during slotZero)`); + // REFACTOR: duplicated Promise.all+retryUntil block checking proposed slot on all nodes; + // replace with a shared helper such as waitUntilAllNodesProposedSlot(nodes, slot, timeout). await Promise.all( nodes.map((node, idx) => retryUntil( @@ -247,6 +259,7 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { // (2) During slotOne: the pipelined proposer for slotTwo broadcasts on top of slotOne → proposed reaches slotTwo. logger.warn(`Waiting for proposed chain to reach slot ${slotTwo} on all nodes (build during slotOne)`); + // REFACTOR: same pattern as above — duplicated Promise.all+retryUntil; extract to helper. await Promise.all( nodes.map((node, idx) => retryUntil( diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts index d15a5a300cdf..ef219a64807d 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts @@ -56,6 +56,11 @@ jest.setTimeout(1000 * 60 * 10); // - All slot-carrying sequencer state events report the target slot (the checkpoint job sets its // state via setStateFn(state, targetSlot)). Slot N+2 is unique to this cycle: the prior cycle // targeted N+1. +// Suite: regression test for sequencer sync logic when L1 slot production stalls mid-slot. +// EpochsTestContext with single-node + mockGossipSubNetwork, prod-seq, interval mining (automine +// during L1 deploy only). Timing: ethSlot=8s (12s CI), aztecSlot=6×ethSlot, epoch=default 6, +// proofSubmissionEpochs=1024, blockDurationMs=8000, inboxLag=2 (v5 always enforces the timetable, so +// the former enforceTimeTable/disableAnvilTestWatcher overrides are gone). No prover. describe('e2e_epochs/epochs_missed_l1_slot', () => { let test: EpochsTestContext; let contract: TestContract; @@ -109,6 +114,9 @@ describe('e2e_epochs/epochs_missed_l1_slot', () => { await test.teardown(); }); + // Asserts that the sequencer enters INITIALIZING_CHECKPOINT for wall-clock slot N+2 while L1 + // mining is paused, proving checkSync no longer stalls when L1 blocks are absent. Then resumes + // mining, waits for the next checkpoint to land, and verifies multi-blocks-per-slot was exercised. it('builds a block after missed L1 slots when previous checkpoint is synced', async () => { const { logger, constants, monitor, context } = test; const eth = context.cheatCodes.eth; @@ -130,6 +138,8 @@ describe('e2e_epochs/epochs_missed_l1_slot', () => { // slot (e.g. in the last L1 slot of L2 slot N), slotFromL1Sync would already be N and the // bug would not be exercised. logger.info('Waiting for a checkpoint published in the first half of its L2 slot...'); + // REFACTOR: raw on-off subscription to ChainMonitor 'checkpoint' event; replace with a + // DSL helper that waits for the first checkpoint satisfying a predicate (e.g. inFirstHalfOfSlot). const checkpointEvent = await executeTimeout( signal => new Promise((res, rej) => { @@ -208,6 +218,8 @@ describe('e2e_epochs/epochs_missed_l1_slot', () => { `Waiting for sequencer to reach INITIALIZING_CHECKPOINT for target slot ${targetSlotForBugFixCycle} ` + `(build slot ${nextSlotNumber}) during mining pause...`, ); + // REFACTOR: raw on-off subscription to sequencer 'state-changed' event; a DSL helper that + // waits for a specific (state, slot) pair would eliminate the manual Promise + signal cleanup. await executeTimeout( signal => new Promise((res, rej) => { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_multi_proof.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_multi_proof.test.ts index ff9802cc5a5d..61fc9cdfe058 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_multi_proof.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_multi_proof.test.ts @@ -12,6 +12,12 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Suite: checks that multiple prover nodes can each submit their own valid proof for the same epoch. +// EpochsTestContext with startProverNode=false (test creates 3 prover nodes manually). Single +// sequencer node. Timing: all defaults (ethSlot=8s/12s CI, aztecSlot=16s/24s, epoch=6, +// proofSubmissionEpochs=1, fake prover). Staggered top-tree-prove delays (v5 patches +// createTopTreeOrchestrator's prove() per node; pre-v5 it patched finalizeEpoch) ensure provers don't +// all land at the same L1 block. describe('e2e_epochs/epochs_multi_proof', () => { let context: EndToEndContext; let rollup: RollupContract; @@ -34,6 +40,10 @@ describe('e2e_epochs/epochs_multi_proof', () => { await test.teardown(); }); + // Creates 3 prover nodes (deferred start), patches each top tree's prove() to stagger by index * + // ethereumSlotDuration (via createTopTreeOrchestrator; pre-v5 this patched finalizeEpoch), then starts + // them all. Waits for epoch 1 to begin, then polls until all 3 provers have submitted proofs for + // epoch 0 via rollup.getHasSubmittedProof. it('submits proofs from multiple prover-nodes', async () => { // Create all three prover nodes without starting them // This allows us to apply the delay patches before any proving begins @@ -79,6 +89,8 @@ describe('e2e_epochs/epochs_multi_proof', () => { logger.info(`Starting epoch 1 with length ${firstEpochLength} after L2 block ${firstEpochLastBlockNum}`); // Wait until all three provers have submitted proofs + // REFACTOR: hand-rolled retryUntil polling loop over Promise.all per-prover submission check; + // a DSL helper like waitForAllProversToSubmit(proverIds, epoch) would centralise this pattern. await retryUntil( async () => { const haveSubmitted = await Promise.all( diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts index 93927ea637d2..731e0bf95d2e 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts @@ -8,6 +8,10 @@ import { EpochsTestContext, WORLD_STATE_CHECKPOINT_HISTORY } from './epochs_test jest.setTimeout(1000 * 60 * 15); +// Suite: verifies that multiple consecutive epochs are proven successfully and that world-state +// checkpoints are pruned after finalization. Uses EpochsTestContext defaults: single node, +// prod-seq, interval mining, ethSlot=8s (12s CI), aztecSlot=16s (24s CI), epoch=6, +// proofSubmissionEpochs=1, fake prover. TARGET_PROVEN_EPOCHS env var controls iteration count. // Assumes one block per checkpoint describe('e2e_epochs/epochs_multiple', () => { let rollup: RollupContract; @@ -25,6 +29,9 @@ describe('e2e_epochs/epochs_multiple', () => { await test.teardown(); }); + // Loops through targetProvenEpochs epochs: waits for each epoch to end, asserts it is proven, + // then verifies the epoch-end block is accessible as a historic block and that earlier blocks + // beyond the checkpoint history window have been purged from world state. it('successfully proves multiple epochs', async () => { const targetProvenEpochs = process.env.TARGET_PROVEN_EPOCHS ? parseInt(process.env.TARGET_PROVEN_EPOCHS) : 3; let epochNumber = 0; diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_optimistic_proving.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_optimistic_proving.parallel.test.ts index 6a4335c5988b..7b2224cd0bef 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_optimistic_proving.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_optimistic_proving.parallel.test.ts @@ -18,6 +18,24 @@ jest.setTimeout(1000 * 60 * 20); /** * E2E tests for optimistic (checkpoint-driven) proving with reorg scenarios. + * + * Setup: a single sequencer/validator node from `EpochsTestContext.setup` plus the context's fake prover-node (no + * `mockGossipSubNetwork`, so no gossip bus), making this a `single-node` test on the production `Sequencer`. Each of the + * six `describe` blocks builds a fresh context in its own `beforeEach` and tears it down in the shared `afterEach`. The + * happy-path pair uses defaults (`numberOfAccounts: 1`; ethSlot=8s local/12s CI, aztecSlot=16s/24s, epoch=6, + * proofSubEpochs=1); the five reorg describes use a faster cadence (ethSlot=4s, aztecSlot=36s, epoch=4 — or 8 for the + * with-replacement case so the replacement lands in-epoch — proofSubEpochs=1000, blockDurationMs=8s, minTxsPerBlock=0, + * anvilSlotsInAnEpoch=32, maxSpeedUpAttempts=0, cancelTxOnTimeout=false). The `prover-node starts mid-epoch` describe + * sets `startProverNode: false` and spins up the prover via `test.createProverNode()` partway through the epoch. + * + * L1 reorgs are driven by `cheatCodes.eth.reorgWithReplacement` and treated as `other-active L1` per the rubric — NOT + * cross-chain bridging — so the file stays `single-node` (mirrors `epochs_partial_proof` / `epochs_sync_after_reorg`). + * Block production is paused/resumed mid-test via the `skipPublishingCheckpointsPercent` node-admin config, and the + * `checkpoint reorg during proving` describe gates top-tree proving with the prover's `beforeTopTreeProve` session hook. + * Anvil runs on interval mining; time advances naturally (the reorgs and `waitUntilNextEpochStarts` do the warping). + * + * Proposed category: `single-node` (epochs/). Heavy hand-rolled coordination throughout — see the inline REFACTOR + * markers below for the raw-async sites a DSL helper should replace. */ describe('e2e_epochs/epochs_optimistic_proving', () => { let context: EndToEndContext; @@ -89,6 +107,8 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { /** epoch -> earliest wall-clock slot at which a CheckpointProver for that epoch was registered. */ const provingStartedAtSlot = new Map(); let stopped = false; + // REFACTOR: hand-rolled setTimeout sampler loop with a `stopped` flag — a polling/observe helper + // (e.g. a sampler that records earliest-observed values per key until disposed) should replace it. const loop = (async () => { while (!stopped) { const { epoch, slot } = test.epochCache.getEpochAndSlotNow(); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts index cca27d9b2b23..7259eca70177 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts @@ -36,6 +36,11 @@ const NODE_COUNT = 4; * distinct proposers P1, P2. P1 is configured via the test-only `skipBroadcastCheckpointProposal` flag to suppress its * CheckpointProposal broadcast while still letting the held last block reach peers. P2 must (a) prune the orphan on * every archiver, and (b) build a fresh checkpoint for S2 that lands on L1. + * + * EpochsTestContext with 4 validator nodes, mockGossipSubNetwork, no prover. Timing: ethSlot=6s, + * aztecSlot=36s, epoch=4, proofSubmissionEpochs=1024, blockDurationMs=8000, inboxLag=2 (v5 always + * enforces the timetable, so the former enforceTimeTable/disableAnvilTestWatcher overrides are gone). + * L1 is time-warped to align with the target S1 build slot. */ describe('e2e_epochs/epochs_orphan_block_prune', () => { let logger: Logger; @@ -47,6 +52,10 @@ describe('e2e_epochs/epochs_orphan_block_prune', () => { await test?.teardown(); }); + // Finds two consecutive slots S1/S2 with distinct proposers. Suppresses P1's CheckpointProposal + // broadcast, waits for the orphan block to appear on all archivers, asserts L2PruneUncheckpointed + // fires on every node for slot S1, then verifies the rebuilt S2 checkpoint lands on L1 with a + // different archive root from the orphan. it('all nodes prune the orphan block and S2 rebuilds the checkpoint chain', async () => { // Build 4 distinct validators (V1..V4). One key per node, no overlap. const validators = times(NODE_COUNT, i => { @@ -93,6 +102,9 @@ describe('e2e_epochs/epochs_orphan_block_prune', () => { // The L1 rollup contract only exposes proposers for epochs whose randao seed is "stable" (i.e. queryable on L1 // right now). When we look too far into the future the contract reverts with `ValidatorSelection__EpochNotStable`. // We handle this by warping L1 forward one epoch at a time and retrying. + // REFACTOR: hand-rolled slot-search loop with per-epoch warp and EpochNotStable retry; a DSL + // helper like findConsecutiveSlotsWithDistinctProposers(minAhead, maxAttempts) would encapsulate + // the epoch-stable query, warp cadence, and candidate-advance logic. let S1: SlotNumber | undefined; let proposerOne: EthAddress | undefined; let proposerTwo: EthAddress | undefined; @@ -197,6 +209,8 @@ describe('e2e_epochs/epochs_orphan_block_prune', () => { // standalone (because of skipBroadcastCheckpointProposal). Every node's proposed tip advances to a block whose // slotNumber === S1. logger.warn(`Waiting for proposed chain to reach slot ${S1} on all nodes (orphan tip from P1)`); + // REFACTOR: Promise.all over per-node retryUntil polling getChainTips; a waitForAllNodesToReach + // helper that takes a predicate over chain tips would avoid this hand-rolled fan-out pattern. await Promise.all( nodes.map((node, idx) => retryUntil( diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof.test.ts index 794a7093aad8..6c486d62ac56 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof.test.ts @@ -9,6 +9,10 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Suite: verifies that manually triggering epoch proving via startProof() results in a partial-proof +// being submitted on L1. EpochsTestContext with single node + fake prover. Timing: ethSlot=default +// (8s/12s CI), aztecSlot=default, epoch=1000 (overridden to a very long epoch so the epoch never +// ends during the test), proofSubmissionEpochs=1 (default). prod-seq, interval mining. describe('e2e_epochs/epochs_partial_proof', () => { let logger: Logger; let monitor: ChainMonitor; @@ -25,12 +29,17 @@ describe('e2e_epochs/epochs_partial_proof', () => { await test.teardown(); }); + // Waits for 4 checkpoints to land, then calls proverNode.startProof(epoch=0) and polls + // ChainMonitor.provenCheckpointNumber until it exceeds 0, confirming a partial proof was + // accepted on-chain. it('submits partial proofs when instructed manually', async () => { // With pipelining, each checkpoint takes ~2 L2 slots on a solo-sequencer setup. await test.waitUntilCheckpointNumber(CheckpointNumber(4), test.L2_SLOT_DURATION_IN_S * 12); logger.info(`Kicking off partial proof`); await test.context.proverNode!.getProverNode()!.startProof(EpochNumber(0)); + // REFACTOR: hand-rolled retryUntil polling ChainMonitor.provenCheckpointNumber; replace with + // test.waitUntilProvenCheckpointNumber(CheckpointNumber(1)) from EpochsTestContext. await retryUntil(() => monitor.provenCheckpointNumber > CheckpointNumber(0), 'proof', 120, 1); logger.info(`Test succeeded with proven checkpoint number ${monitor.provenCheckpointNumber}`); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof_multi_root.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof_multi_root.test.ts index 60116fb31795..8df67ef56029 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof_multi_root.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_partial_proof_multi_root.test.ts @@ -27,13 +27,15 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); -// Since AZIP-14 the Outbox can hold up to MAX_CHECKPOINTS_PER_EPOCH partial-proof roots per epoch, one -// per `numCheckpointsInEpoch` (1-indexed). This test stages three progressively-deeper roots for -// the same epoch by driving the EpochTestSettler after each checkpointed tx, then tests: -// (a) consuming a message uses the smallest covering root the client helper picks, -// (b) the user can consume the same message against any covering root (both K=1 and K=2 cover -// a message in checkpoint 0), and the shared bitmap prevents double-consume across K, and -// (c) a message whose checkpoint is not yet covered by any root yields no witness. +// Suite: verifies the AZIP-14 partial-proof multi-root Outbox design. Drives an EpochTestSettler +// manually to stage progressively deeper partial-proof roots (K=1, 2, 3) for the same epoch, then +// asserts: (a) the node picks the smallest covering root, (b) any covering root produces a valid +// consume tx, (c) the shared bitmap blocks double-spend, and (d) K=4 can be staged later. +// EpochsTestContext: single node, no prover, prod-seq, interval mining. Timing: ethSlot=default +// (8s/12s CI), aztecSlot=default, epoch=1000, proofSubmissionEpochs=1024 (v5: the disableAnvilTestWatcher +// override was removed and a perBlockAllocationMultiplier=1.3 was added so the first block of the +// now-up-to-5-block checkpoint has enough DA budget for the TestContract deploy tx). The test actively +// calls the Outbox L1 contract to consume L2-to-L1 messages → cross-chain. describe('e2e_epochs/epochs_partial_proof_multi_root', () => { let test: EpochsTestContext; let logger: Logger; @@ -88,6 +90,10 @@ describe('e2e_epochs/epochs_partial_proof_multi_root', () => { await test.teardown(); }); + // Deploys TestContract, advances past the setup epoch, then sends 4 L2-to-L1-message txs each + // in a fresh slot. After the first 3, calls settler.handleEpochReadyToProve to insert K=1, 2, 3 + // partial-proof roots. Asserts Outbox roots match locally-computed values, verifies consume() + // succeeds under each covering K, and confirms the shared bitmap prevents re-consumption. it('stages 3 partial-proof roots and lets messages consume against any covering root', async () => { const { wallet } = test.context; const [from] = test.context.accounts; @@ -219,6 +225,8 @@ describe('e2e_epochs/epochs_partial_proof_multi_root', () => { // Consume msg2 against the smallest covering root the node picks (K=2). { + // REFACTOR: hand-rolled retryUntil waiting for L2ToL1 membership witness availability; a + // DSL helper like waitForL2ToL1MembershipWitness(txHash, leaf) would encapsulate the retry. const witness = await retryUntil( () => node.getL2ToL1MembershipWitness(sends[1].receipt.txHash, sends[1].leaf), 'K=2 membership witness', diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_at_boundary.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_at_boundary.parallel.test.ts index d0477f967d68..ba1cabdd509b 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_at_boundary.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_at_boundary.parallel.test.ts @@ -26,6 +26,12 @@ const NODE_COUNT = 3; type PreparingEvent = Parameters[0]; type PublishedEvent = Parameters[0]; +// Suite: 5 parallel scenarios testing the interaction between the proof submission deadline and +// the pipelining boundary slot. EpochsTestContext: 3 validator nodes + 1 prover node, +// mockGossipSubNetwork, skipInitialSequencer. Timing: ethSlot=12s, aztecSlot=3×12=36s, +// epoch=default 6, proofSubmissionEpochs=1 (overridden per test via setupTest), blockDurationMs=6s, +// inboxLag=2 (v5 always enforces the timetable, so the former enforceTimeTable/disableAnvilTestWatcher +// overrides are gone). The Delayer is used to steer proof tx timing. describe('e2e_epochs/epochs_proof_at_boundary', () => { let context: EndToEndContext; let logger: Logger; @@ -96,6 +102,8 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { }; const computeBoundarySlot = async () => { + // REFACTOR: hand-rolled retryUntil polling for first checkpoint; replace with + // test.waitUntilCheckpointNumber(CheckpointNumber(1)) from EpochsTestContext. await retryUntil( async () => { await test.monitor.run(true); @@ -197,6 +205,9 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { await test?.teardown(); }); + // Delays the proof tx so it mines in the L2 slot immediately before the boundary. Asserts the + // boundary slot's checkpoint-published event fires (proven pin path was taken), the proof receipt + // is success, and the proof timestamp falls within (boundaryTs - L2_SLOT, boundaryTs). it('proof lands during slot build and checkpoint succeeds at boundary', async () => { // The proof for the unproven epoch lands AFTER the boundary slot's pipelined build starts but // BEFORE the publisher's preCheck. The proven pin lets the boundary checkpoint build before @@ -242,6 +253,9 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { logger.warn(`Test passed. Final tip checkpoint=${test.monitor.checkpointNumber}`); }); + // Sanity check: prover runs naturally; proof lands well before the boundary. Asserts the boundary + // checkpoint still publishes (proven pin is defensive only here) and the proof timestamp is + // earlier than boundaryTs - L2_SLOT_DURATION_IN_S. it('proof lands well before deadline and checkpoint succeeds at boundary', async () => { // Sanity check: the prover runs on its natural schedule, so the proof lands well before the // boundary epoch. By the time the boundary slot is built `tips.proven` is already advanced @@ -273,6 +287,9 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { expect(Number(test.monitor.checkpointNumber)).toBeGreaterThanOrEqual(Number(boundaryPublished!.checkpoint)); }); + // Cancels the proof tx before starting the prover so the deadline is missed. Asserts no + // checkpoint-published event fires for the boundary slot, and that the first post-boundary + // checkpoint lands within 2 slots of the boundary (chain recovers via on-chain prune). it('proof never lands so no checkpoint submission is attempted', async () => { // The boundary slot's build applies the proven pin, but the publisher's preCheck rejects the // propose tx because the proof never landed. After the prune fires on a later slot, a fresh @@ -307,6 +324,9 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { expect(getEpochAtSlot(firstPostBoundary.slot, test.constants)).toBe(boundaryEpoch); }); + // Pauses proposing for slot N-1 (the slot before the boundary) so no proposed parent exists at + // the boundary. Proof still lands early. Asserts the boundary checkpoint publishes and no + // preparing event recorded a hadProposedParent=true for the boundary slot. it('proof lands without a proposed parent and boundary checkpoint succeeds', async () => { // The slot before the boundary is paused so the boundary slot's build does not see a proposed // parent. The proof still lands well before the deadline, so the proven pin is defensive only @@ -344,6 +364,8 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { expect(Number(test.monitor.checkpointNumber)).toBeGreaterThanOrEqual(Number(boundaryPublished!.checkpoint)); }); + // Combines no-proposed-parent (slot N-1 paused) with cancelled proof tx. Asserts boundary did + // not propose, and the first post-boundary checkpoint lands within 2 slots (on-chain prune path). it('proof never lands without a proposed parent so no checkpoint submission is attempted', async () => { // Same as the no-parent variant above but with the proof never landing. The proven pin fires // (no parent + prune is due) but the publisher's preCheck rejects the propose, so no diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_fails.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_fails.parallel.test.ts index 8da131ebd02c..1cb820ae5f63 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_fails.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_fails.parallel.test.ts @@ -22,6 +22,11 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Suite: 2 parallel scenarios testing proof-submission failure paths. EpochsTestContext with single +// sequencer node, no initial prover (prover nodes created in test bodies). Timing: ethSlot=8s, +// aztecSlot=2×8=16s, epoch=8, proofSubmissionEpochs=1 (default), blockDurationMs=3s, +// cancelTxOnTimeout=false, inboxLag=2 (v5 always enforces the timetable, so the former enforceTimeTable +// override is gone). Prover Delayer steers proof tx timing. describe('e2e_epochs/epochs_proof_fails', () => { let context: EndToEndContext; let l1Client: ViemClient; @@ -55,6 +60,10 @@ describe('e2e_epochs/epochs_proof_fails', () => { await test.teardown(); }); + // Delays the proof tx until after epoch 2 starts (past the submission deadline). Waits for + // epoch 1 to end, then epoch 2 to begin, and polls until the rollup checkpoint number drops + // below the pre-rollback value. Asserts the delayed proof receipt is reverted and the + // post-rollback chain tip is in epoch 2. it('does not allow submitting proof after epoch end', async () => { // Here we cause a re-org by not publishing the proof for epoch 0 until after the end of epoch 1. // The proof will be rejected and a re-org will take place via the next post-deadline propose tx. @@ -91,6 +100,8 @@ describe('e2e_epochs/epochs_proof_fails', () => { // checkpoint number rather than a fixed timestamp because the exact slot that triggers the // prune depends on poll timing (see comment above). await test.waitUntilEpochStarts(EpochNumber(2)); + // REFACTOR: hand-rolled retryUntil polling rollup.getCheckpointNumber for rollback detection; + // a DSL helper like waitForRollback(checkpoint) would make the intent clearer. await retryUntil( async () => (await rollup.getCheckpointNumber()) < checkpointBeforeRollback, 'rollup rolled back', @@ -114,6 +125,11 @@ describe('e2e_epochs/epochs_proof_fails', () => { logger.warn(`Test succeeded`); }); + // Injects a sleep delay of epochDuration * L2_SLOT_DURATION into each top tree's prove() (patched + // via createTopTreeOrchestrator with a jest spy; v5 split epoch proving into per-checkpoint top + // trees, replacing the former finalizeEpoch patch), ensuring the prover misses the epoch 1 deadline. + // Asserts that after the gated prove resolves, no proof tx was submitted (the prover aborted), and + // the proven checkpoint number remained 0 through epoch 1. it('aborts proving if end of next epoch is reached', async () => { // Create prover node after test setup to avoid early proving const proverNode = await test.createProverNode({ cancelTxOnTimeout: false, maxSpeedUpAttempts: 0 }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts index 1dc83cb637c5..4c068e530a33 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts @@ -21,6 +21,11 @@ jest.setTimeout(1000 * 60 * 10); // messages first. This causes a block header mismatch (different state roots, fees, mana) when a tx consumes // a message that was added to the L1-to-L2 message tree in the same block — the prover reverts the tx while // the sequencer processes it successfully. +// +// EpochsTestContext: 1 node + fake prover, prod-seq, interval mining. Timing: all defaults (ethSlot=8s/12s +// CI, aztecSlot=16s/24s, epoch=6, proofSubmissionEpochs=1), minTxsPerBlock=1 (v5: the disableAnvilTestWatcher +// override was removed). Cross-chain: writes to L1 Inbox (sendL1ToL2Message), then claims the message in a +// public L2 function. describe('e2e_epochs/epochs_proof_public_cross_chain', () => { let context: EndToEndContext; let logger: Logger; @@ -41,6 +46,9 @@ describe('e2e_epochs/epochs_proof_public_cross_chain', () => { await test.teardown(); }); + // Sends an L1→L2 message via the Inbox, waits for it to be synced, then sends a public tx + // consuming the message in the same block it lands. Waits for the epoch proof to cover that + // block, then confirms the message cannot be consumed a second time. it('submits proof with a tx with public l1-to-l2 message claim', async () => { // Deploy a contract that consumes L1 to L2 messages await context.aztecNodeAdmin.setConfig({ minTxsPerBlock: 0 }); @@ -74,6 +82,8 @@ describe('e2e_epochs/epochs_proof_public_cross_chain', () => { // Wait until a proof lands for the transaction logger.warn(`Waiting for proof for tx ${txReceipt.txHash} mined at ${txReceipt.blockNumber!}`); + // REFACTOR: hand-rolled retryUntil polling aztecNode.getBlockNumber('proven'); replace with + // test.waitUntilProvenCheckpointNumber or a waitForProvenBlock(blockNumber) DSL helper. await retryUntil( async () => { const provenBlockNumber = await context.aztecNode.getBlockNumber('proven'); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_simple_block_building.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_simple_block_building.test.ts index 7a3f203021d2..64d1f52afd67 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_simple_block_building.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_simple_block_building.test.ts @@ -25,6 +25,12 @@ jest.setTimeout(1000 * 60 * 10); const NODE_COUNT = 3; const TX_COUNT = 8; +// Suite: verifies that 3 validator nodes can build blocks without sequencer errors. Uses a +// lightweight RPC-only initial node (skipInitialSequencer), mockGossipSubNetwork, no prover. +// Timing: ethSlot=12s, aztecSlot=3×12=36s, epoch=default 6, proofSubmissionEpochs=1024, +// blockDurationMs=6s, inboxLag=2 (v5 always enforces the timetable, so the former enforceTimeTable/ +// disableAnvilTestWatcher overrides are gone). Pre-proved txs sent from hardcoded +// genesis-funded account (no on-chain account deploy needed). // Sets up a lightweight RPC-only node without any account deployment, registers a test contract // locally, then spawns NODE_COUNT validator nodes connected via a mocked gossip sub network. // Mines N txs across N blocks, checking that no sequencer errors occur during block building. @@ -79,6 +85,8 @@ describe('e2e_epochs/epochs_simple_block_building', () => { await test.teardown(); }); + // Pre-proves TX_COUNT transactions emitting unique nullifiers, sends them, waits for all to mine, + // then asserts no fail events were emitted by any of the 3 sequencers during the run. it('builds blocks without any errors', async () => { const sequencers = nodes.map(node => node.getSequencer()!); const { failEvents } = test.watchSequencerEvents(sequencers, i => ({ validator: validators[i].attester })); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_sync_after_reorg.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_sync_after_reorg.test.ts index 11e077ca2e09..42386221e4b7 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_sync_after_reorg.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_sync_after_reorg.test.ts @@ -10,6 +10,11 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Suite: regression test ensuring a new node can sync world-state after an unpruned reorg +// (issue #12206). EpochsTestContext with single node, no prover, prod-seq, interval mining. +// Timing: all defaults (ethSlot=8s/12s CI, aztecSlot=16s/24s, epoch=6, proofSubmissionEpochs=1). +// The test stops the sequencer mid-run, advances into epoch 2 via waitUntilEpochStarts, then +// creates a second node and verifies it syncs cleanly despite the reorg window. describe('e2e_epochs/epochs_sync_after_reorg', () => { let context: EndToEndContext; let logger: Logger; @@ -29,7 +34,10 @@ describe('e2e_epochs/epochs_sync_after_reorg', () => { await test.teardown(); }); - // Regression for https://github.com/AztecProtocol/aztec-packages/issues/12206 + // Regression for https://github.com/AztecProtocol/aztec-packages/issues/12206. + // Waits for 5 checkpoints, stops the main sequencer node, waits for epoch 2 to start (creating + // a reorg window), then creates a fresh non-validator node with a 10s timeout and verifies its + // block number is 0 (it did not get stuck on a reorg'd block). it('new node can sync world-state after unpruned reorg', async () => { // Wait until there are a few checkpoints in there // With pipelining, each checkpoint takes ~2 L2 slots (the sequencer must wait for diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_upload_failed_proof.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_upload_failed_proof.test.ts index b5ee69c32160..865ddc658179 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_upload_failed_proof.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_upload_failed_proof.test.ts @@ -19,6 +19,11 @@ import { EpochsTestContext } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 10); +// Suite: verifies that a failed epoch-proving job uploads its state to a file store and that +// rerunEpochProvingJob can re-prove from the downloaded data on a fresh instance. Uses +// EpochsTestContext with a prover configured to use a temp file:// URL as the epoch failure store. +// Timing: all defaults (ethSlot=8s/12s CI, aztecSlot=16s/24s, epoch=6, proofSubmissionEpochs=1, +// fake prover). The test tears down mid-run and re-proves via a standalone helper. describe('e2e_epochs/epochs_upload_failed_proof', () => { let context: EndToEndContext; let logger: Logger; @@ -52,6 +57,11 @@ describe('e2e_epochs/epochs_upload_failed_proof', () => { await tryRmDir(rerunDownloadDir, logger); }); + // Makes the prover's top-tree prove always throw (v5 uses the session's topTreeProveOverride hook; + // pre-v5 it patched finalizeEpoch), intercepts tryUploadSessionFailure (pre-v5 tryUploadEpochFailure) + // to capture the upload URL, then waits for epoch 1 to start and for the upload to complete. Tears + // down the live context, downloads the proving job data, and re-runs it via rerunEpochProvingJob with + // fake proofs on a fresh config. it('uploads failed proving job state and re-runs it on a fresh instance', async () => { // Make initial prover node fail to prove, via the session's top-tree-prove hook. const proverNode = test.proverNodes[0].getProverNode() as TestProverNode; @@ -74,6 +84,8 @@ describe('e2e_epochs/epochs_upload_failed_proof', () => { // Wait until the start of epoch one so prover node starts proving epoch 0, // and wait for the data to be uploaded to the remote file store await test.waitUntilEpochStarts(1); + // REFACTOR: hand-rolled retryUntil polling a local variable for the upload completion; a DSL + // helper or a Promise-based event on the prover node would avoid the polling loop. await retryUntil(() => epochUploadUrl !== undefined, 'Upload epoch failure', 240, 1); // Stop everything, we're going to prove on a fresh instance diff --git a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts index 6f6c33012a27..330ddf9f5425 100644 --- a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts @@ -12,6 +12,9 @@ import { expectTokenBalance, mintTokensToPrivate } from './fixtures/token_utils. import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; +// Tests the Escrow contract: withdrawing to a recipient, access-control enforcement, and +// multi-key batch operations. Uses setup(2, AUTOMINE_E2E_OPTS) with one node, automine sequencer, +// and two funded accounts (owner, recipient). A fresh escrow and token are deployed in beforeEach. describe('e2e_escrow_contract', () => { let wallet: TestWallet; @@ -61,6 +64,8 @@ describe('e2e_escrow_contract', () => { afterEach(() => teardown(), 30_000); + // Calls escrowContract.withdraw(token, 30, recipient) as owner and asserts recipient balance + // increases by 30 and escrow decreases from 100 to 70. it('withdraws funds from the escrow contract', async () => { await expectTokenBalance(wallet, token, owner, 0n, logger); await expectTokenBalance(wallet, token, recipient, 0n, logger); @@ -77,6 +82,7 @@ describe('e2e_escrow_contract', () => { await expectTokenBalance(wallet, token, escrowContract.address, 70n, logger); }); + // Simulates withdraw from recipient (non-owner) and expects a rejection (owner check). it('refuses to withdraw funds as a non-owner', async () => { await expect( escrowContract.methods @@ -86,6 +92,8 @@ describe('e2e_escrow_contract', () => { ).rejects.toThrow(); }); + // Mints 50 to owner, then uses BatchCall to transfer 10 from owner and withdraw 20 from escrow + // in the same tx. Asserts recipient ends up with 30 total. it('moves funds using multiple keys on the same tx (#1010)', async () => { logger.info(`Minting funds in token contract to ${owner}`); const mintAmount = 50n; diff --git a/yarn-project/end-to-end/src/e2e_event_logs.test.ts b/yarn-project/end-to-end/src/e2e_event_logs.test.ts index ce1410241cd8..c563539f0ba8 100644 --- a/yarn-project/end-to-end/src/e2e_event_logs.test.ts +++ b/yarn-project/end-to-end/src/e2e_event_logs.test.ts @@ -22,6 +22,10 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; +// Covers private-log event emission and retrieval (encrypted events via getPrivateEvents) and +// public-log event emission and retrieval (unencrypted events via getPublicEvents), including nested +// events with struct fields and tagging-cache reconciliation against kernel squashing. Uses a single +// automine node with two genesis-funded Schnorr accounts and a deployed TestLogContract. describe('Logs', () => { let testLogContract: TestLogContract; jest.setTimeout(TIMEOUT); @@ -50,7 +54,12 @@ describe('Logs', () => { afterAll(() => teardown()); + // Covers encrypted private-log emission, public unencrypted-log emission, and nested-event + // struct decoding. Verifies getPrivateEvents and getPublicEvents return correctly typed payloads. describe('functionality around emitting an encrypted log', () => { + // Sends 5 emit_encrypted_events txs in parallel, then queries getPrivateEvents for two event + // types and checks counts and field values. Also sends 5 unencrypted txs and verifies + // getPublicEvents round-trips correctly. it('emits multiple events as private logs and decodes them', async () => { const preimages = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple, 5>; @@ -121,6 +130,8 @@ describe('Logs', () => { ); }); + // Sends 5 emit_unencrypted_events txs and retrieves events via getPublicEvents; verifies + // field values round-trip correctly for both ExampleEvent0 and ExampleEvent1. it('emits multiple unencrypted events as public logs and decodes them', async () => { const preimage = makeTuple(5, makeTuple.bind(undefined, 4, Fr.random)) as Tuple, 5>; @@ -177,6 +188,8 @@ describe('Logs', () => { ); }); + // Emits an ExampleNestedEvent with struct field and verifies that getPublicEvents correctly + // decodes nested struct fields (a, b, c, extra_value). it('decodes public events with nested structs', async () => { const a = Fr.random(); const b = Fr.random(); @@ -211,6 +224,8 @@ describe('Logs', () => { // between calls, // 2. across separate transactions that interact with the same contract function, confirming proper persistence // of the cache contents in the database (TaggingDataProvider) after transaction proving completes. + // Sends two emit_encrypted_events_nested txs (4 and 2 nesting levels), fetches raw private logs, + // and asserts that all tags within a tx are unique and tags are globally unique across both txs. it('produces unique tags for encrypted logs across nested calls and different transactions', async () => { let tx1Tags: string[]; // With 4 nestings we have 5 total calls, each emitting 2 logs => 10 logs @@ -263,6 +278,9 @@ describe('Logs', () => { }); }); + // Regression suite for the PXE tagging-cache / kernel-squashing interaction (issue #22949). + // The kernel may squash some log emissions; the PXE must reconcile its recorded index ranges + // against surviving logs to avoid Conflicting range errors on subsequent txs. describe('tagging cache reconciliation against kernel squashing', () => { // Regression test for https://github.com/AztecProtocol/aztec-packages/issues/22949. // @@ -271,6 +289,8 @@ describe('Logs', () => { // its delivery log with it). The PXE must reconcile the recorded ranges against the kernel's surviving private // logs before persisting them, otherwise a subsequent tx sharing the same tagging secret hits a // `Conflicting range` error when its tagging sync re-derives the range from on-chain data and notices a mismatch. + // Calls deliver_squashed_and_surviving_notes twice in sequence (same tagging secret for both). + // Verifies no Conflicting range error on the second call after the first squashes a log index. it('does not throw `Conflicting range` across consecutive squashing txs sharing a tagging secret', async () => { // Each call reserves two indexes for the (sender, sender, contract) tagging secret and squashes the first // delivery's (note, nullifier, log) triple. Pre-fix, the second call's tagging sync would observe that the diff --git a/yarn-project/end-to-end/src/e2e_event_only.test.ts b/yarn-project/end-to-end/src/e2e_event_only.test.ts index e2d0a79b7ef1..845f1c82d839 100644 --- a/yarn-project/end-to-end/src/e2e_event_only.test.ts +++ b/yarn-project/end-to-end/src/e2e_event_only.test.ts @@ -12,6 +12,7 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; /// Tests that a private event can be obtained for a contract that does not work with notes. +// Single automine node, one genesis-funded account, EventOnlyContract deployed in beforeAll. describe('EventOnly', () => { let eventOnlyContract: EventOnlyContract; jest.setTimeout(TIMEOUT); @@ -31,6 +32,8 @@ describe('EventOnly', () => { afterAll(() => teardown()); + // Sends emit_event_for_msg_sender, then calls getPrivateEvents for TestEvent and asserts that + // exactly one event is returned with the correct value field. it('emits and retrieves a private event for a contract with no notes', async () => { const value = Fr.random(); const { receipt: tx } = await eventOnlyContract.methods diff --git a/yarn-project/end-to-end/src/e2e_expiration_timestamp.test.ts b/yarn-project/end-to-end/src/e2e_expiration_timestamp.test.ts index 69ca90026ab2..a42b04e229dc 100644 --- a/yarn-project/end-to-end/src/e2e_expiration_timestamp.test.ts +++ b/yarn-project/end-to-end/src/e2e_expiration_timestamp.test.ts @@ -10,6 +10,10 @@ import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; import { proveInteraction } from './test-wallet/utils.js'; +// Covers transaction expiration-timestamp enforcement: setting a valid expiration succeeds, setting +// one below the mined block timestamp fails at prove time, and setting one that is then warped past +// by L1 time causes rejection at submission. Uses a single automine node; L1 time is warped via +// cheatCodes.eth.warp in the invalidation tests. describe('e2e_expiration_timestamp', () => { let wallet: TestWallet; let defaultAccountAddress: AztecAddress; @@ -34,6 +38,8 @@ describe('e2e_expiration_timestamp', () => { afterAll(() => teardown()); + // Expiration is set two slots ahead of the latest block, so it is above the next slot's + // timestamp. Expects the tx to prove and land without error. describe('when requesting expiration timestamp higher than the one of a mined block', () => { let expirationTimestamp: bigint; @@ -51,6 +57,8 @@ describe('e2e_expiration_timestamp', () => { describe('with no enqueued public calls', () => { const enqueuePublicCall = false; + // Proves a private-only tx and asserts the expirationTimestamp in the tx data equals the + // requested value. it('sets the expiration timestamp', async () => { const tx = await proveInteraction( wallet, @@ -62,6 +70,7 @@ describe('e2e_expiration_timestamp', () => { // See compute_tx_expiration_timestamp.ts for the rounding logic. }); + // Sends a private-only tx with a future expiration and expects it to be mined successfully. it('does not invalidate the transaction', async () => { await contract.methods .set_expiration_timestamp(expirationTimestamp, enqueuePublicCall) @@ -72,6 +81,8 @@ describe('e2e_expiration_timestamp', () => { describe('with an enqueued public call', () => { const enqueuePublicCall = true; + // Proves a hybrid (private+public) tx and asserts the expirationTimestamp equals the + // requested value. it('sets expiration timestamp', async () => { const tx = await proveInteraction( wallet, @@ -81,6 +92,7 @@ describe('e2e_expiration_timestamp', () => { expect(tx.data.expirationTimestamp).toEqual(expirationTimestamp); }); + // Sends a hybrid tx with a future expiration and expects it to be mined successfully. it('does not invalidate the transaction', async () => { await contract.methods .set_expiration_timestamp(expirationTimestamp, enqueuePublicCall) @@ -89,6 +101,9 @@ describe('e2e_expiration_timestamp', () => { }); }); + // Expiration is set one timestamp unit below the next slot's start, so it is provable + // (expiration > anchor block) but rejected at submission. The invalidation tests also warp L1 + // time via cheatCodes.eth.warp to force expiration in the node's slot check. describe('when requesting expiration timestamp lower than the next block', () => { let expirationTimestamp: bigint; @@ -107,6 +122,8 @@ describe('e2e_expiration_timestamp', () => { describe('with no enqueued public calls', () => { const enqueuePublicCall = false; + // Proves a private-only tx; even though expiration < nextSlot the prove-time check passes + // because expiration > anchor block timestamp. Asserts the field is set. it('sets expiration timestamp', async () => { const tx = await proveInteraction( wallet, @@ -116,6 +133,8 @@ describe('e2e_expiration_timestamp', () => { expect(tx.data.expirationTimestamp).toEqual(expirationTimestamp); }); + // Proves a tx with a safe expiration, then warps L1 time past it via cheatCodes.eth.warp, + // then sends the proven tx and expects TX_ERROR_INVALID_EXPIRATION_TIMESTAMP. it('invalidates the transaction', async () => { await runInvalidatesTest(enqueuePublicCall); }); @@ -124,6 +143,8 @@ describe('e2e_expiration_timestamp', () => { describe('with an enqueued public call', () => { const enqueuePublicCall = true; + // Proves a hybrid tx; even though expiration < nextSlot the prove-time check passes. Asserts + // the expirationTimestamp field is set. it('sets expiration timestamp', async () => { const tx = await proveInteraction( wallet, @@ -133,6 +154,8 @@ describe('e2e_expiration_timestamp', () => { expect(tx.data.expirationTimestamp).toEqual(expirationTimestamp); }); + // Proves a hybrid tx with a safe expiration, warps L1 time past it, then expects + // TX_ERROR_INVALID_EXPIRATION_TIMESTAMP on send. it('invalidates the transaction', async () => { await runInvalidatesTest(enqueuePublicCall); }); @@ -176,6 +199,7 @@ describe('e2e_expiration_timestamp', () => { } }); + // Expiration is set below the already-mined block's timestamp, so proving itself must fail. describe('when requesting expiration timestamp lower than the one of a mined block', () => { let expirationTimestamp: bigint; @@ -191,6 +215,8 @@ describe('e2e_expiration_timestamp', () => { describe('with no enqueued public calls', () => { const enqueuePublicCall = false; + // Sends a private-only tx with an expiration already below the current block timestamp; + // expects rejection before it can be proven. it('fails to prove the tx', async () => { await expect( contract.methods @@ -203,6 +229,7 @@ describe('e2e_expiration_timestamp', () => { describe('with an enqueued public call', () => { const enqueuePublicCall = true; + // Sends a hybrid tx with an expiration below the current block; expects prove-time rejection. it('fails to prove the tx', async () => { await expect( contract.methods diff --git a/yarn-project/end-to-end/src/e2e_fee_asset_price_oracle.test.ts b/yarn-project/end-to-end/src/e2e_fee_asset_price_oracle.test.ts index 8aa4e2b652a0..390ba0c865ac 100644 --- a/yarn-project/end-to-end/src/e2e_fee_asset_price_oracle.test.ts +++ b/yarn-project/end-to-end/src/e2e_fee_asset_price_oracle.test.ts @@ -14,6 +14,11 @@ import { MNEMONIC, PIPELINING_SETUP_OPTS } from './fixtures/fixtures.js'; import { getLogger, setup, startAnvil } from './fixtures/utils.js'; import { MockStateView, diffInBps } from './shared/mock_state_view.js'; +// Covers the on-chain fee-asset price oracle convergence mechanism. Starts its own Anvil instance, +// deploys a MockStateView (etched at the real StateView address), then runs a single node with +// PIPELINING_SETUP_OPTS (prod seq, ethereumSlotDuration=4s, aztecSlotDuration=12s, minTxsPerBlock=0). +// Verifies that the rollup's getEthPerFeeAsset converges toward the oracle price across checkpoints +// via retryUntil polling. describe('FeeAssetPriceOracle E2E', () => { jest.setTimeout(15 * 60 * 1000); @@ -74,6 +79,8 @@ describe('FeeAssetPriceOracle E2E', () => { delete process.env.ETHEREUM_HOSTS; }); + // Sets the oracle price up 2.5% then polls rollup.getEthPerFeeAsset until it matches within 1 bps. + // Then moves the oracle price down 0.5% and polls again. Asserts final price tracked both moves. it('on-chain price converges toward oracle price over multiple checkpoints', async () => { // Move the price up 2.5% (2 moves of 1% and another smaller) // Wait until we are within 1 bps or the price @@ -89,6 +96,8 @@ describe('FeeAssetPriceOracle E2E', () => { const initialOnChainPrice = await rollup.getEthPerFeeAsset(); logger.info(`Initial on-chain price: ${initialOnChainPrice}, target oracle price: ${targetOraclePrice}`); + // REFACTOR: hand-rolled retryUntil polling loop waiting for price convergence; a DSL helper for + // "wait until rollup price is within N bps of oracle" would make the intent clearer. await retryUntil( async () => { const currentPrice = await rollup.getEthPerFeeAsset(); @@ -105,6 +114,7 @@ describe('FeeAssetPriceOracle E2E', () => { await mockStateView.setEthPerFeeAsset(targetOraclePrice2); logger.info(`Set uniswap price to ${targetOraclePrice2}`); + // REFACTOR: second hand-rolled retryUntil polling loop for price convergence; same as above. await retryUntil( async () => { const currentPrice = await rollup.getEthPerFeeAsset(); 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 d42178e61c64..ddff2bccb8bc 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 @@ -25,6 +25,9 @@ import { FeesTest } from './fees_test.js'; // ~24s/tx pipelined cadence, exceeding the default 5 min hook window. jest.setTimeout(15 * 60 * 1000); +// Fee payment during account contract initialization. Uses FeesTest (prod sequencer, pipelining preset: +// ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0), 1 account, fake in-proc prover node, and +// GasBridgingTestHarness for L1↔L2 fee-juice bridging via FeeJuicePortal. describe('e2e_fees account_init', () => { const t = new FeesTest('account_init', 1); @@ -86,7 +89,10 @@ describe('e2e_fees account_init', () => { await initBalances(); }); + // Scenarios where the newly created account itself covers its own deployment fee. describe('account pays its own fee', () => { + // Alice bridges fee juice to Bob's undeployed address via FeeJuicePortal, then Bob deploys his account + // paying from that bridged balance. Asserts the balance decreases by exactly the transaction fee. it('pays natively in the Fee Juice after Alice bridges funds', async () => { const mintAmount = await t.feeJuiceBridgeTestHarness.l1TokenManager.getMintAmount(); await t.mintAndBridgeFeeJuice(aliceAddress, bobsAddress); @@ -99,6 +105,8 @@ describe('e2e_fees account_init', () => { await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); }); + // Bob claims fee juice atomically in the same account-deployment tx via + // FeeJuicePaymentMethodWithClaim. No pre-bridging required from Alice. it('pays natively in the Fee Juice by bridging funds themselves', async () => { const claim = await t.feeJuiceBridgeTestHarness.prepareTokensOnL1(bobsAddress); const paymentMethod = new FeeJuicePaymentMethodWithClaim(bobsAddress, claim); @@ -110,6 +118,8 @@ describe('e2e_fees account_init', () => { await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([claim.claimAmount - tx.transactionFee!]); }); + // Alice mints bananas to Bob's undeployed address; Bob deploys through the BananaCoin FPC using + // PrivateFeePaymentMethod. Asserts the refund note reduces Bob's banana balance by the actual fee. it('pays privately through an FPC', async () => { // Alice mints bananas to Bob const mintedBananas = await t.feeJuiceBridgeTestHarness.l1TokenManager.getMintAmount(); @@ -141,6 +151,8 @@ describe('e2e_fees account_init', () => { await expect(t.getGasBalanceFn(bananaFPC.address)).resolves.toEqual([fpcsInitialGas - actualFee]); }); + // Bob deploys using PublicFeePaymentMethod: Alice mints bananas to Bob's public balance, then Bob + // deploys with the FPC deducting from that public balance. Asserts the FPC received the fee. it('pays publicly through an FPC', async () => { const mintedBananas = await t.feeJuiceBridgeTestHarness.l1TokenManager.getMintAmount(); await bananaCoin.methods.mint_to_public(bobsAddress, mintedBananas).send({ from: aliceAddress }); @@ -172,7 +184,10 @@ describe('e2e_fees account_init', () => { }); }); + // Scenarios where a third party (Alice) sponsors the deployment fee for Bob's account. describe('another account pays the fee', () => { + // Alice mints bananas to Bob, then deploys Bob's account on his behalf paying from her own fee-juice + // balance. Bob's account is then used immediately to send a private FPC-paid tx. it('pays natively in the Fee Juice', async () => { // bob generates the private keys for his account on his own const bobsPublicKeys = (await deriveKeys(bobsSecretKey)).publicKeys; diff --git a/yarn-project/end-to-end/src/e2e_fees/bridging_race.notest.ts b/yarn-project/end-to-end/src/e2e_fees/bridging_race.notest.ts index 5da5f10569e6..e9b6792542cf 100644 --- a/yarn-project/end-to-end/src/e2e_fees/bridging_race.notest.ts +++ b/yarn-project/end-to-end/src/e2e_fees/bridging_race.notest.ts @@ -15,6 +15,10 @@ jest.setTimeout(300_000); // Regression for https://github.com/AztecProtocol/aztec-packages/issues/12366 // Similar to e2e_fees/account_init but with no automine +// Disabled (.notest.ts): this regression was covered by fixes applied at each "wait for two blocks" +// site in the codebase; keeping the file as reference for the original race scenario. Uses FeesTest +// with prod sequencer (ethSlot=4s, aztecSlot=8s, inboxLag default, minTxsPerBlock=0) and +// GasBridgingTestHarness for L1↔L2 fee-juice bridging. Single account, fake in-proc prover node. describe('e2e_fees bridging_race', () => { const ETHEREUM_SLOT_DURATION = 4; const AZTEC_SLOT_DURATION = ETHEREUM_SLOT_DURATION * 2; @@ -50,6 +54,10 @@ describe('e2e_fees bridging_race', () => { bobsAddress = bobsAccountManager.address; }); + // Reproduces a timing race where an L1→L2 fee-juice bridge message lands just before the end of an + // L2 slot, causing the archiver to miss it. The fix was to wait for the archiver to see the message + // before waiting for the required two-block confirmation. The sleep injected into approve() simulates + // the near-slot-boundary timing. it('Alice bridges funds to Bob', async () => { // Tweak the token manager so the bridging happens immediately before the end of the current L2 slot // This caused the message to be "not in state" when tried to be used @@ -59,6 +67,8 @@ describe('e2e_fees bridging_race', () => { await origApprove(amount, address, addressName); const sleepTime = (Number(t.chainMonitor.checkpointTimestamp) + AZTEC_SLOT_DURATION) * 1000 - Date.now() - 500; logger.info(`Sleeping for ${sleepTime}ms until near end of L2 slot before sending L1 fee juice to L2 inbox`); + // REFACTOR: hand-rolled slot-boundary sleep; replace with a timing helper that derives the remaining + // slot time from the chain monitor's slot boundaries rather than computing it inline. await sleep(sleepTime); }; diff --git a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts index 36df2220b67f..5cfd2c4f58f9 100644 --- a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts @@ -21,6 +21,10 @@ import { ensureAuthRegistryPublished } from '../fixtures/setup.js'; import { expectMapping } from '../fixtures/utils.js'; import { FeesTest } from './fees_test.js'; +// Fee behaviour when transactions revert. Uses FeesTest (prod sequencer, pipelining preset: +// ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0, aztecEpochDuration=4, +// aztecProofSubmissionEpochs=640), fake in-proc prover node, and GasBridgingTestHarness for +// L1↔L2 fee-juice bridging. Auto-proving is disabled after setup so tests control proving themselves. describe('e2e_fees failures', () => { // FeesTest.setup + applyFPCSetup chains many dependent txs which run at the // ~24s/tx pipelined cadence, exceeding the default 5 min hook window. @@ -56,6 +60,9 @@ describe('e2e_fees failures', () => { await t.teardown(); }); + // Submits a tx that reverts in public app logic while using the private FPC path. Asserts that the + // fee is still paid from the FPC's gas balance, Alice gets a banana refund note, and the sequencer + // reward on L1 equals the fee minus prover fee and burn after the epoch is proven. it('reverts transactions but still pays fees using PrivateFeePaymentMethod', async () => { const outrageousPublicAmountAliceDoesNotHave = t.ALICE_INITIAL_BANANAS * 5n; const privateMintedAlicePrivateBananas = t.ALICE_INITIAL_BANANAS; @@ -90,6 +97,8 @@ describe('e2e_fees failures', () => { await expectMapping(t.getGasBalanceFn, [aliceAddress, bananaFPC.address], [initialAliceGas, initialFPCGas]); // We wait until the proven chain is caught up so all previous fees are paid out. + // REFACTOR: manual advanceToNextEpoch + catchUpProvenChain sequence; replace with a single + // waitForEpochProven() helper on FeesTest that encapsulates this pattern. await t.cheatCodes.rollup.advanceToNextEpoch(); await t.catchUpProvenChain(); @@ -161,6 +170,8 @@ describe('e2e_fees failures', () => { ); }); + // Same as above but using the public FPC path. Verifies the public banana balances are updated + // correctly for Alice and the FPC even when the app logic transfer reverts. it('reverts transactions but still pays fees using PublicFeePaymentMethod', async () => { const outrageousPublicAmountAliceDoesNotHave = t.ALICE_INITIAL_BANANAS * 5n; const publicMintedAlicePublicBananas = t.ALICE_INITIAL_BANANAS; @@ -243,6 +254,8 @@ describe('e2e_fees failures', () => { ); }); + // A tx whose fee-payment setup phase (BuggedSetupFeePaymentMethod) demands more than maxFee causes + // both local simulation and the sequencer to reject the tx outright — the tx is never included. it('fails transaction that error in setup', async () => { const OutrageousPublicAmountAliceDoesNotHave = BigInt(100e12); @@ -271,6 +284,9 @@ describe('e2e_fees failures', () => { ).rejects.toThrow(/Transaction (0x)?[0-9a-fA-F]{64} was dropped/i); }); + // A tx whose teardown gas limit is zero (BuggedTeardownFeePaymentMethod) reverts in teardown but + // is still included in a block. Asserts the tx revert code is REVERTED, Alice was charged up to the + // fee limit in setup, and the epoch can still be proven after the revert. it('includes transaction that error in teardown', async () => { /** * We trigger an error in teardown by having the "FPC" call a function that reverts. @@ -352,6 +368,8 @@ describe('e2e_fees failures', () => { await waitForProven(aztecNode, receipt, { provenTimeout }); }); + // Sends a tx that reverts in both app logic and teardown, then advances the epoch and waits for + // the block to be proven. Ensures a double-revert tx can be proven without hanging the prover. it('proves transaction where both app logic and teardown revert', async () => { const outrageousPublicAmountAliceDoesNotHave = t.ALICE_INITIAL_BANANAS * 5n; diff --git a/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts index df16714a7ce1..eec8f435de8a 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts @@ -12,6 +12,9 @@ import { PIPELINING_SETUP_OPTS } from '../fixtures/fixtures.js'; import type { TestWallet } from '../test-wallet/test_wallet.js'; import { FeesTest } from './fees_test.js'; +// Direct Fee Juice payment flows. Uses FeesTest (prod sequencer, pipelining preset: ethSlot=4s, +// aztecSlot=12s, inboxLag=2, minTxsPerBlock=0), 1 account (Alice), fake in-proc prover node, and +// GasBridgingTestHarness for L1↔L2 fee-juice bridging. Bob's account is pre-deployed by Alice. describe('e2e_fees Fee Juice payments', () => { // FeesTest.setup + applyFundAliceWithBananas chains many dependent txs which run at the // ~24s/tx pipelined cadence, exceeding the default 5 min hook window. @@ -48,6 +51,7 @@ describe('e2e_fees Fee Juice payments', () => { await t.teardown(); }); + // Bob has no fee juice; these tests verify failure cases before bridging. describe('without initial funds', () => { beforeAll(async () => { expect( @@ -55,6 +59,7 @@ describe('e2e_fees Fee Juice payments', () => { ).toEqual(0n); }); + // Confirms that simulate() throws "Not enough balance" when the sender has zero fee juice. it('fails to simulate a tx', async () => { await expect( feeJuiceContract.methods @@ -63,12 +68,15 @@ describe('e2e_fees Fee Juice payments', () => { ).rejects.toThrow(/Not enough balance for fee payer to pay for transaction/i); }); + // Confirms that send() throws "Insufficient fee payer balance" when the sender has zero fee juice. it('fails to send a tx', async () => { await expect( feeJuiceContract.methods.check_balance(0n).send({ from: bobAddress, fee: { gasSettings } }), ).rejects.toThrow(/Invalid tx: Insufficient fee payer balance/i); }); + // Bob bridges fee juice from L1 and claims it atomically in the same tx via + // FeeJuicePaymentMethodWithClaim. Asserts the post-tx balance equals claimAmount minus fee. it('claims bridged funds and pays with them on the same tx', async () => { const claim = await t.feeJuiceBridgeTestHarness.prepareTokensOnL1(bobAddress); const paymentMethod = new FeeJuicePaymentMethodWithClaim(bobAddress, claim); @@ -85,7 +93,10 @@ describe('e2e_fees Fee Juice payments', () => { }); }); + // Alice has pre-funded fee juice; these tests verify normal Fee Juice payment flows. describe('with initial funds', () => { + // Alice sends a public token transfer paying the fee natively in Fee Juice; asserts the balance + // decreases by the transaction fee. it('sends tx with payment in Fee Juice with public calls', async () => { const { result: initialBalance } = await feeJuiceContract.methods .balance_of_public(aliceAddress) @@ -102,6 +113,8 @@ describe('e2e_fees Fee Juice payments', () => { expect(endBalance).toBeLessThan(initialBalance); }); + // Same as above but the tx is a private-only transfer (no public calls), ensuring the fee + // is deducted from Alice's fee-juice balance even in the no-public-call path. it('sends tx fee payment in Fee Juice with no public calls', async () => { const { result: initialBalance } = await feeJuiceContract.methods .balance_of_public(aliceAddress) diff --git a/yarn-project/end-to-end/src/e2e_fees/fee_settings.test.ts b/yarn-project/end-to-end/src/e2e_fees/fee_settings.test.ts index 3ccb27a3e26e..916ed4c9d69a 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fee_settings.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fee_settings.test.ts @@ -16,6 +16,12 @@ import type { TestWallet } from '../test-wallet/test_wallet.js'; import { proveInteraction } from '../test-wallet/utils.js'; import { FeesTest } from './fees_test.js'; +// Fee oracle and wallet fee-padding behaviour under L1 base-fee spikes and governance fee-config bumps. +// Uses FeesTest with a custom timing preset (ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0, +// aztecProofSubmissionEpochs=640, manaTarget=4M, walletMinFeePadding=30) and fake in-proc prover node. +// No token bridging involved — all L1 interaction is L1 base-fee cheat codes and Rollup oracle calls. +// (Category: single-node despite using FeesTest, since no cross-chain token transfer or fee-juice +// portal bridging occurs in any test body — L1 is active only for oracle updates.) describe('e2e_fees fee settings', () => { let aztecNode: AztecNode; let cheatCodes: CheatCodes; @@ -61,6 +67,7 @@ describe('e2e_fees fee settings', () => { await t.teardown(); }); + // Tests that wallet fee padding correctly handles L2 fee spikes driven by L1 base-fee changes. describe('setting max fee per gas', () => { // Drive an organic L2 fee bump via an L1 base-fee spike. On mainnet, L1 base fees fluctuate // organically with L1 demand and dominate `feePerL2Gas` (the rollup's L1 gas oracle samples @@ -114,6 +121,8 @@ describe('e2e_fees fee settings', () => { ); t.logger.info(`Targeting L1 base fee ${targetL1BaseFee} (current ${currentL1BaseFee})`); + // REFACTOR: hand-rolled retryUntil loop that mines L1 blocks and rotates the oracle; replace with + // a helper on RollupCheatCodes that abstracts the L1-base-fee-spike + oracle-rotation retry. return await retryUntil( async () => { await cheatCodes.eth.setNextBlockBaseFeePerGas(targetL1BaseFee); @@ -140,6 +149,8 @@ describe('e2e_fees fee settings', () => { // Pick a baseline from the post-checkpoint chain state. The prove step itself is // made deterministic by prepareTxsWithMockedMinFees below. const getCurrentMinFeesAfterCheckpoint = async (checkpointedBlock: BlockNumber) => { + // REFACTOR: hand-rolled retryUntil polling for a checkpointed block number; replace with a + // waitUntilCheckpointedBlockNumber(node, blockNumber) helper in the e2e fixture utilities. return await retryUntil( async () => { const currentCheckpointedBlock = await aztecNode.getBlockNumber('checkpointed'); @@ -189,6 +200,9 @@ describe('e2e_fees fee settings', () => { } }; + // Prepares two txs at the same stable fee snapshot (one with no padding, one with default 30x + // padding), then spikes the L1 base fee so the L2 oracle rotates upward. Asserts the no-padding + // tx is rejected for insufficient fee while the padded tx mines successfully. it('handles min fee spikes with default padding', async () => { const stableMinFees = await getCurrentMinFeesAfterCheckpoint(testContractDeployBlock); const { txWithNoPadding, txWithDefaultPadding } = await prepareTxsWithMockedMinFees(stableMinFees, stableMinFees); @@ -216,6 +230,9 @@ describe('e2e_fees fee settings', () => { await expect(txWithDefaultPadding.send()).resolves.toBeDefined(); }); + // Prepares one tx against a synthetically higher fee snapshot and another against a lower one, + // then spikes L2 fees between the lower and higher values. Asserts both mine, proving the higher + // snapshot correctly covers the post-spike fee without relying on the default padding. it('reproduces the stale fee snapshot race deterministically', async () => { // The previous test bumped the proving cost, setting FeeLib's provingCostLastUpdate. // Clear the 30-day cooldown so bumpL2Fees below can land. @@ -249,6 +266,9 @@ describe('e2e_fees fee settings', () => { await expect(txWithDefaultPadding.send()).resolves.toBeDefined(); }); + // Regression test for A-1057: a governance fee-config bump between proposer build and L1 submit + // invalidates the pipelined checkpoint. Asserts the chain skips the bad slot and resumes producing + // checkpoints, and that a fresh tx prepared after the bump mines under default padding. // Regression test for A-1057. Under pipelining, the proposer for slot N starts building the // checkpoint header (and bakes `manaMinFee` into `gasFees.feePerL2Gas`) during slot N-1. If // governance executes `setProvingCostPerMana` or `updateManaTarget` between that build and the @@ -275,6 +295,8 @@ describe('e2e_fees fee settings', () => { // `checkpointed` tip must strictly advance. const RECOVERY_TARGET = CheckpointNumber.add(checkpointBefore, 3); const RECOVERY_BUDGET_SECONDS = AZTEC_SLOT_DURATION * 6; + // REFACTOR: hand-rolled retryUntil polling for checkpoint number; replace with a + // waitForCheckpointNumber(node, target) helper from EpochsTestContext or a shared utility. await retryUntil( async () => (await aztecNode.getCheckpointNumber('checkpointed')) >= RECOVERY_TARGET, `chain advances at least ${RECOVERY_TARGET - checkpointBefore} checkpoints past governance bump`, diff --git a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts index 0e38620091da..535123a12ec0 100644 --- a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts @@ -34,6 +34,8 @@ function waitForSequencerIdle(sequencer: Sequencer, timeout = 30000): Promise { const timer = setTimeout(() => { sequencer.off('state-changed', handler); @@ -52,6 +54,9 @@ function waitForSequencerIdle(sequencer: Sequencer, timeout = 30000): Promise { // FeesTest.setup + applyFPCSetup + applyFundAliceWithBananas chains many dependent txs which run // at the pipelined cadence, exceeding the default 5 min hook window. @@ -122,6 +127,11 @@ describe('e2e_fees gas_estimation', () => { teardownGasLimits: inspect(estimatedGas.teardownGasLimits), }); + // Simulates a public token transfer with includeMetadata=true and derives zero-padded gas limits from + // the reported gasUsed (v5: the old estimateGas=true / estimatedGasPadding=0 flow was replaced by + // simulate(includeMetadata) + estimateGasLimits, which yields gasLimits == manaUsed), then sends two + // copies — one with the estimated gas limits, one without. Asserts the estimated tx and the default tx + // pay the same fee, and that the estimated teardown gas is zero for a Fee Juice payment (no teardown work). it('estimates gas with Fee Juice payment method', async () => { const sim = await makeTransferRequest().simulate({ from: aliceAddress, @@ -159,6 +169,8 @@ describe('e2e_fees gas_estimation', () => { expect(estimatedFee).toEqual(withEstimate.transactionFee!); }); + // Same flow but with a public FPC payment method. Asserts the estimated teardown gas limits are + // smaller than the default and that the estimated tx fee is lower than the unestimated tx fee. it('estimates gas with public payment method', async () => { const gasSettingsForEstimation = new GasSettings( new Gas(GAS_ESTIMATION_DA_GAS_LIMIT, GAS_ESTIMATION_L2_GAS_LIMIT), @@ -200,6 +212,10 @@ describe('e2e_fees gas_estimation', () => { expect(estimatedFee).toEqual(withEstimate.transactionFee!); }); + // Deploys a BananaCoin contract, simulating with includeMetadata=true and deriving zero-padded gas + // limits from gasUsed (v5: replaces the old estimateGas=true flow — see note above), then sends two + // deployments — one with estimated limits, one with defaults. Asserts both pay the same fee and + // estimated teardown is zero. it('estimates gas for public contract initialization with Fee Juice payment method', async () => { const deployMethod = () => BananaCoin.deploy(wallet, aliceAddress, 'TKN', 'TKN', 8); const deployOpts = (limits?: Pick) => { diff --git a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts index 7eb6043ee19c..86e35bfcf0ca 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts @@ -15,6 +15,10 @@ import type { TestWallet } from '../test-wallet/test_wallet.js'; import { proveInteraction } from '../test-wallet/utils.js'; import { FeesTest } from './fees_test.js'; +// Private fee payment via BananaCoin FPC (PrivateFeePaymentMethod). Uses FeesTest (prod sequencer, +// pipelining preset: ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0, aztecEpochDuration=4, +// aztecProofSubmissionEpochs=640), fake in-proc prover node, and GasBridgingTestHarness for L1↔L2 +// fee-juice bridging. Auto-proving is disabled after setup so tests control epoch advancement. describe('e2e_fees private_payment', () => { // FeesTest.setup + applyFPCSetup + applyFundAliceWithBananas chains many dependent txs which run at the // ~24s/tx pipelined cadence, exceeding the default 5 min hook window. @@ -79,6 +83,8 @@ describe('e2e_fees private_payment', () => { ]); }); + // Alice transfers private bananas to Bob using PrivateFeePaymentMethod. Verifies sequencer rewards + // on L1 equal fee minus prover fee and burn, and Alice's banana balance decreases by fee + transfer. it('pays fees for tx that dont run public app logic', async () => { /** * PRIVATE SETUP (1 nullifier for tx) @@ -155,6 +161,8 @@ describe('e2e_fees private_payment', () => { ); }); + // Alice mints private bananas to herself while paying via FPC. Asserts the FPC banana public + // balance increases by the fee and Alice's private balance increases net of the fee. it('pays fees for tx that creates notes in private', async () => { /** * PRIVATE SETUP @@ -199,6 +207,8 @@ describe('e2e_fees private_payment', () => { ); }); + // Alice transfers bananas from public to private (creating a note via public app logic) while paying + // via FPC. Asserts both private and public balances change correctly and the FPC receives its fee. it('pays fees for tx that creates notes in public', async () => { /** * PRIVATE SETUP @@ -247,6 +257,8 @@ describe('e2e_fees private_payment', () => { ); }); + // A BatchCall combines a private transfer and a public-to-private shield in one tx while paying via + // FPC. Verifies all four balance deltas (Alice private, Alice public, Bob private, FPC public). it('pays fees for tx that creates notes in both private and public', async () => { const amountTransferredInPrivate = 1n; const amountTransferredToPrivate = 2n; @@ -302,6 +314,8 @@ describe('e2e_fees private_payment', () => { ); }); + // Deploys a BananaFPC with no fee-juice funding, then tries to use it as a fee payer. + // Asserts the tx is rejected with "Insufficient fee payer balance" before execution. it('rejects txs that dont have enough balance to cover gas costs', async () => { // deploy a copy of bananaFPC but don't fund it! const { contract: bankruptFPC } = await FPCContract.deploy(wallet, bananaCoin.address, aliceAddress).send({ @@ -321,6 +335,8 @@ describe('e2e_fees private_payment', () => { }); // TODO(#7694): Remove this test once the lacking feature in TXE is implemented. + // Passes max_fee=1 (effectively zero) to PrivateFeePaymentMethod so the funded amount check fires. + // Asserts simulation throws "max fee not enough to cover tx fee". it('insufficient funded amount is correctly handled', async () => { // We call arbitrary `private_get_name(...)` function just to check the correct error is triggered. await expect( diff --git a/yarn-project/end-to-end/src/e2e_fees/public_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/public_payments.test.ts index df4cc32f479a..082d753cda69 100644 --- a/yarn-project/end-to-end/src/e2e_fees/public_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/public_payments.test.ts @@ -12,6 +12,9 @@ import { PIPELINING_SETUP_OPTS, getPaddedMaxFeesPerGas } from '../fixtures/fixtu import { expectMapping } from '../fixtures/utils.js'; import { FeesTest } from './fees_test.js'; +// Public fee payment via BananaCoin FPC (PublicFeePaymentMethod). Uses FeesTest (prod sequencer, +// pipelining preset: ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0), fake in-proc prover +// node, and GasBridgingTestHarness for L1↔L2 fee-juice bridging (the FPC setup bridges fee juice). describe('e2e_fees public_payment', () => { // FeesTest.setup + applyFPCSetup + applyFundAliceWithBananas chains many dependent txs which run // at the ~24s/tx pipelined cadence, exceeding the default 5 min hook window. @@ -64,6 +67,8 @@ describe('e2e_fees public_payment', () => { ]); }); + // Alice sends 10 bananas to Bob using PublicFeePaymentMethod. Asserts Alice's banana balance + // decreases by bananasToSendToBob + fee, FPC public balance increases by fee, and FPC gas decreases. it('pays fees for tx that make public transfer', async () => { const bananasToSendToBob = 10n; const { receipt: tx } = await bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/sponsored_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/sponsored_payments.test.ts index 6e1f89dbff21..5ada3eed9802 100644 --- a/yarn-project/end-to-end/src/e2e_fees/sponsored_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/sponsored_payments.test.ts @@ -11,6 +11,11 @@ import { PIPELINING_SETUP_OPTS, getPaddedMaxFeesPerGas } from '../fixtures/fixtu import { expectMapping } from '../fixtures/utils.js'; import { FeesTest } from './fees_test.js'; +// Sponsored fee payment via SponsoredFPC (SponsoredFeePaymentMethod). Uses FeesTest (prod sequencer, +// pipelining preset: ethSlot=4s, aztecSlot=12s, inboxLag=2, minTxsPerBlock=0), fake in-proc prover +// node, and GasBridgingTestHarness for L1↔L2 fee-juice bridging (the SponsoredFPC is funded at +// genesis via fundSponsoredFPC; the test exercises the sponsored path where the user pays no fee juice +// directly). Also used as a code snippet in the documentation (docs:start/end:sponsored_fpc_simple). describe('e2e_fees sponsored_public_payment', () => { // FeesTest.setup + applySponsoredFPCSetup + applyFundAliceWithBananas chains many dependent txs which run // at the ~24s/tx pipelined cadence, exceeding the default 5 min hook window. @@ -60,6 +65,8 @@ describe('e2e_fees sponsored_public_payment', () => { ]); }); + // Alice transfers bananas to Bob via SponsoredFeePaymentMethod. The SponsoredFPC covers the fee + // from its own gas balance; Alice's gas balance is unaffected and only her banana balance changes. it('pays fees for tx that makes a public transfer', async () => { // docs:start:sponsored_fpc_simple const bananasToSendToBob = 10n; diff --git a/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts b/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts index d9a96bb69f8d..be5dbb143b7d 100644 --- a/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts +++ b/yarn-project/end-to-end/src/e2e_genesis_timestamp.test.ts @@ -7,6 +7,13 @@ import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { type EndToEndContext, setup } from './fixtures/utils.js'; import { proveInteraction } from './test-wallet/utils.js'; +// Verifies that genesis-anchored transactions (proved while PXE is pinned to block 0) can be +// included in blocks after block 1, and that PXE can prove transactions anchored to genesis even +// after the chain has advanced (public data tree diverged). Uses AUTOMINE_E2E_OPTS with +// advancePastGenesis=false, two deployable accounts in additionallyFundedAccounts, and pxe +// syncChainTip='proven' so the anchor stays at genesis until a real proof lands, which never happens +// in these tests (no prover node running). (v5: replaced skipAccountDeployment with +// advancePastGenesis=false + explicit additionallyFundedAccounts.) describe('e2e_genesis_timestamp', () => { let context: EndToEndContext; @@ -51,11 +58,15 @@ describe('e2e_genesis_timestamp', () => { const awaitBlockCheckpointed = async () => { const { aztecNode } = context; + // REFACTOR: hand-rolled retryUntil polling on block number and checkpoint number; a helper like + // waitForBlockNumber / waitForCheckpointNumber would replace both calls. await retryUntil(async () => (await aztecNode.getBlockNumber()) >= 1, 'wait for block >= 1', 60); await retryUntil(async () => (await aztecNode.getCheckpointNumber()) >= 1, 'wait for checkpoint >= 1', 60); logger.info(`Block number after advancing: ${await aztecNode.getBlockNumber()}`); }; + // Proves an account-deploy tx while at block 0, mines an empty block via mineBlock(), then + // sends the genesis-anchored proven tx and asserts it lands after block 1. it('can include genesis-anchored tx in a block after block 1', async () => { const { aztecNode } = context; @@ -81,6 +92,8 @@ describe('e2e_genesis_timestamp', () => { // Regression for an issue where PXE failed to prove txs while anchored to block zero // if there were new blocks mined that modified the public data tree. + // Sends a first genesis-anchored account deploy (modifies public data tree), then proves and + // sends a second genesis-anchored deploy for a different account and asserts it also lands. it('can generate genesis-anchored tx after chain advances when PXE anchor is pinned to zero', async () => { const { aztecNode } = context; diff --git a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts index 4e25df6d934e..1ebc44b62c01 100644 --- a/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts +++ b/yarn-project/end-to-end/src/e2e_kernelless_simulation.test.ts @@ -29,6 +29,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; * Demonstrates the capability of simulating a transaction without executing the kernels, allowing * the bypass of many checks and a healthy improvement in speed. Kernelless simulations should aim * to be as close as possible to reality, so their output can be used to calculate gas usage + * + * Uses a single automine node with three funded accounts (admin, liquidityProvider, swapper), + * an AMM with two token pairs, and various test contracts deployed in beforeAll. */ describe('Kernelless simulation', () => { let teardown: () => Promise; @@ -79,6 +82,8 @@ describe('Kernelless simulation', () => { afterAll(() => teardown()); + // Covers authwit-request discovery via kernelless simulation, gas estimation comparison between + // kernelless and with-kernels paths, and fee payer identity propagation. describe('Authwits and gas', () => { type Balance = { token0: bigint; @@ -99,6 +104,9 @@ describe('Kernelless simulation', () => { }; } + // Simulates add_liquidity in kernelless-override mode, captures the two offchain authwit + // requests, derives and verifies authwit hashes, then sends the real tx with authwitnesses + // created from the offchain effect inner hashes. it('adds liquidity without authwits', async () => { const lpBalancesBefore = await getWalletBalances(liquidityProviderAddress); @@ -232,6 +240,8 @@ describe('Kernelless simulation', () => { ).resolves.toBeDefined(); }); + // Simulates a swap in both kernelless-override and full modes and asserts L2Gas and DA gas + // estimates match. Also verifies feePayer is identical between the two paths. it('produces matching gas estimates and fee payer between kernelless and with-kernels simulation', async () => { const swapperBalancesBefore = await getWalletBalances(swapperAddress); const ammBalancesBefore = await getAmmBalances(); @@ -292,6 +302,8 @@ describe('Kernelless simulation', () => { }); }); + // Verifies that kernelless simulation correctly models note squashing (transient create+nullify + // patterns) and produces the same gas estimates as the full-kernels path. describe('Note squashing', () => { let pendingNoteHashesContract: PendingNoteHashesContract; @@ -301,6 +313,8 @@ describe('Kernelless simulation', () => { })); }); + // Simulates test_insert_then_get_then_nullify_all_in_nested_calls in both modes; checks that + // L2Gas and DA gas estimates are identical between kernelless and full paths. it('squashing produces same gas estimates as with-kernels path', async () => { const mintAmount = 42n; @@ -336,6 +350,8 @@ describe('Kernelless simulation', () => { }); }); + // Verifies that the #[authorize_once] macro correctly serializes struct parameters into the + // offchain CallAuthorizationRequest, including field count and decoded argument values. describe('authorize_once with multi-field struct parameters', () => { let authWitTestContract: AuthWitTestContract; let proxy: GenericProxyContract; @@ -347,6 +363,8 @@ describe('Kernelless simulation', () => { ]); }); + // Simulates auth_with_struct via a proxy in kernelless mode; asserts that the emitted + // offchain effect has 6 serialized fields and decodes to the correct 4-parameter call args. it('emits offchain effect with correct serialized args length for struct parameters', async () => { const structData = { a: Fr.random(), b: Fr.random(), c: Fr.random() }; const amount = Fr.random(); @@ -393,6 +411,8 @@ describe('Kernelless simulation', () => { }); }); + // Verifies that kernelless simulation resolves settled note-hash read requests against the + // on-chain note hash tree via findLeavesIndexes. describe('read request verification', () => { let pendingNoteHashesContract: PendingNoteHashesContract; @@ -402,6 +422,8 @@ describe('Kernelless simulation', () => { })); }); + // Inserts a note with full kernels, then simulates get_then_nullify_note in kernelless mode + // and checks that findLeavesIndexes was called to look up the settled note hash. it('verifies settled read requests against the note hash tree', async () => { const mintAmount = 100n; @@ -430,7 +452,11 @@ describe('Kernelless simulation', () => { }); }); + // Checks that kernelless simulation produces the same gas estimates as the full-kernels path for + // Schnorr and ECDSA account contract deployments. describe('account contract deployment', () => { + // Creates a fresh Schnorr account manager, simulates its deploy in both kernelless and full + // modes, and asserts L2Gas and DA gas limits are identical. it('simulates Schnorr account deployment and gas matches with-kernels counterpart', async () => { const signingKey = Fq.random(); const accountManager = await wallet.createAccount({ @@ -457,6 +483,8 @@ describe('Kernelless simulation', () => { expect(kernellessGas.totalGas.l2Gas).toEqual(withKernelsGas.totalGas.l2Gas); }); + // Creates a fresh EcdsaK account manager, simulates its deploy in both kernelless and full + // modes, and asserts L2Gas and DA gas limits are identical. it('simulates ECDSA account deployment and gas matches with-kernels counterpart', async () => { const signingKey = randomBytes(32); const accountManager = await wallet.createAccount({ diff --git a/yarn-project/end-to-end/src/e2e_keys.test.ts b/yarn-project/end-to-end/src/e2e_keys.test.ts index a12b02623c2c..249c3e8a283b 100644 --- a/yarn-project/end-to-end/src/e2e_keys.test.ts +++ b/yarn-project/end-to-end/src/e2e_keys.test.ts @@ -24,6 +24,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; const TIMEOUT = 300_000; +// Covers cryptographic key derivation and usage: nhk_app-based nullification detection and +// ovsk_app retrieval via the TestContract. Single automine node, one funded Schnorr account, +// TestContract deployed in beforeAll. describe('Keys', () => { jest.setTimeout(TIMEOUT); @@ -49,6 +52,8 @@ describe('Keys', () => { afterAll(() => teardown()); + // Demonstrates that an observer holding nhk_app and the contract address can detect when a note + // they did not create has been nullified, by scanning all note hashes and re-deriving nullifiers. describe('using nhk_app to detect nullification', () => { // This test checks that it is possible to detect that a note has been nullified just by using nhk_app. Note // that this only works for non-transient notes as transient ones never emit a note hash which makes it @@ -65,6 +70,8 @@ describe('Keys', () => { // is impossible to detect with this scheme. // Another example is withdrawing from DeFi and then immediately spending the funds. In this case, we would // need nhk_app and the contract address of the DeFi contract to detect the nullification of the initial note. + // Creates a note, asserts 0 nullified notes. Destroys the note, scans all blocks for matching + // nullifiers derived from nhk_app and asserts exactly 1 nullified note. it('nhk_app and contract address are enough to detect note nullification', async () => { const masterNullifierHidingKey = deriveMasterNullifierHidingKey(secret); const nhkApp = await computeAppNullifierHidingKey(masterNullifierHidingKey, testContract.address); @@ -110,7 +117,10 @@ describe('Keys', () => { }; }); + // Verifies that the on-chain get_ovsk_app circuit function returns the same ovsk_app as the + // TypeScript derivation path (deriveMasterOutgoingViewingSecretKey + computeAppSecretKey). describe('ovsk_app', () => { + // Derives ovsk_app in TS, calls get_ovsk_app on-chain, and compares the field values. it('gets ovsk_app', async () => { // Derive the ovpk_m_hash from the account secret. Use `hashPublicKey` (the // domain-separated hash over `[x, y]`) rather than `Point.hash()` (which hashes diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts index 53ac2043cfbb..852171edec68 100644 --- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts @@ -112,6 +112,12 @@ const numberOfConsecutiveBlocks = 3; jest.setTimeout(1000000); +// Low-level integration tests for SequencerPublisher: building checkpoints, publishing with and +// without attestations, handling L1 tx cancellation/speedup, and invalidating bad checkpoints. +// Custom wiring: starts its own anvil directly via startAnvil(), deploys L1 contracts, builds a +// real NativeWorldStateService + ServerWorldStateSynchronizer, and creates a SequencerPublisher +// directly — no AztecNodeService, no PXE. EthCheatCodesWithState drives time. Each describe block +// has its own beforeEach calling the local setup() function. describe('L1Publisher integration', () => { let l1Client: ExtendedViemWalletClient; let l1ContractAddresses: L1ContractAddresses; diff --git a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts index 57c1ab475d64..c6ced9afcd12 100644 --- a/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_with_wall_time.test.ts @@ -14,6 +14,10 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; jest.setTimeout(1000 * 60 * 10); +// Verifies that the production sequencer (prod seq, PIPELINING_SETUP_OPTS: ethereumSlotDuration=4s, +// aztecSlotDuration=12s, minTxsPerBlock=0) can produce multiple blocks with batches of transactions +// in real wall-time (no automine). Exercises the proposer pipelining path with a pre-registered +// validator (initialValidators) that matches the publisher private key. describe('e2e_l1_with_wall_time', () => { let logger: Logger; let teardown: () => Promise; @@ -51,9 +55,13 @@ describe('e2e_l1_with_wall_time', () => { afterEach(() => teardown?.()); + // Submits deploymentsPerBlock txs in 4 sequential rounds, waits for each batch to be mined, + // and asserts all tx hashes are eventually confirmed. it('should produce blocks with a bunch of transactions', async () => { for (let i = 0; i < numberOfBlocks; i++) { const txHashes = await submitTxsTo(wallet, defaultAccountAddress, deploymentsPerBlock, logger); + // REFACTOR: Promise.all over individual waitForTx calls; a waitForTxs batch helper would + // consolidate this into a single polling loop. await Promise.all( txHashes.map((hash, j) => { logger.info(`Waiting for tx ${i}-${j}: ${hash.toString()} to be mined`); diff --git a/yarn-project/end-to-end/src/e2e_large_public_event.test.ts b/yarn-project/end-to-end/src/e2e_large_public_event.test.ts index f8247b01d179..eb29962fabc9 100644 --- a/yarn-project/end-to-end/src/e2e_large_public_event.test.ts +++ b/yarn-project/end-to-end/src/e2e_large_public_event.test.ts @@ -14,6 +14,7 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; /// Tests that events exceeding MAX_EVENT_SERIALIZED_LEN can be emitted publicly. +// Single automine node, one funded account, LargePublicEventContract deployed in beforeAll. describe('LargePublicEvent', () => { let contract: LargePublicEventContract; jest.setTimeout(TIMEOUT); @@ -35,6 +36,8 @@ describe('LargePublicEvent', () => { afterAll(() => teardown()); + // Sends emit_large_event with 11 random Fr fields, retrieves via getPublicEvents, and asserts + // the returned event's data array matches. it('emits and retrieves a public event with more than MAX_EVENT_SERIALIZED_LEN fields', async () => { const data = Array.from({ length: 11 }, () => Fr.random()); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index a629781d0c83..56a044664ab4 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -19,6 +19,10 @@ import { ensureAuthRegistryPublished, setup } from './fixtures/utils.js'; import { LendingAccount, LendingSimulator, TokenSimulator } from './simulators/index.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; +// Covers the full lifecycle of the Lending contract (deposit, borrow, repay, withdraw) in both +// private (🥸) and public paths, with and without authorization witnesses. Uses a single automine +// node; LendingSimulator tracks expected state and verifies after each test. Note that the +// lendingSim.progressSlots calls advance slot time on the simulator (not L1 time directly). describe('e2e_lending_contract', () => { jest.setTimeout(100_000); let wallet: TestWallet; @@ -133,6 +137,8 @@ describe('e2e_lending_contract', () => { lendingSim.observeBlockTimestamp(Number(block!.header.globalVariables.timestamp)); }; + // Mints collateral and stable-coin tokens to lendingAccount in both public and private, + // records the mints in the simulator, and sets the price feed to 2e9. it('Mint assets for later usage', async () => { await priceFeedContract.methods.set_price(0n, 2n * 10n ** 9n).send({ from: defaultAccountAddress }); @@ -154,6 +160,8 @@ describe('e2e_lending_contract', () => { lendingSim.collateralAsset.mintPublic(lendingAccount.address, 10000n); }); + // Calls lending.init() to set price feed, LTV, and asset addresses; reads the block timestamp + // to initialize lendingSim.time so it matches the on-chain accumulator. it('Initialize the contract', async () => { logger.info('Initializing contract'); const { receipt } = await lendingContract.methods @@ -166,7 +174,10 @@ describe('e2e_lending_contract', () => { lendingSim.time = Number(block!.header.globalVariables.timestamp); }); + // Covers private and public deposit paths into the lending collateral pool. describe('Deposits', () => { + // Creates a transfer_to_public authwit, advances slots, calls deposit_private for own account, + // and updates simulator state. it('Depositing 🥸 : 💰 -> 🏦', async () => { const activationThreshold = 420n; const authwitNonce = Fr.random(); @@ -201,6 +212,8 @@ describe('e2e_lending_contract', () => { lendingSim.depositPrivate(lendingAccount.address, await lendingAccount.key(), activationThreshold); }); + // Creates a transfer_to_public authwit, calls deposit_private on behalf of a public recipient, + // and updates simulator state with the public key. it('Depositing 🥸 on behalf of recipient: 💰 -> 🏦', async () => { const activationThreshold = 421n; const authwitNonce = Fr.random(); @@ -235,6 +248,7 @@ describe('e2e_lending_contract', () => { lendingSim.depositPrivate(lendingAccount.address, lendingAccount.address.toField(), activationThreshold); }); + // Sets a public authwit for transfer_in_public, calls deposit_public, and updates simulator. it('Depositing: 💰 -> 🏦', async () => { const activationThreshold = 211n; @@ -273,7 +287,9 @@ describe('e2e_lending_contract', () => { }); }); + // Covers private and public borrow paths from the lending pool. describe('Borrow', () => { + // Advances slots, calls borrow_private using the lendingAccount secret, and updates simulator. it('Borrow 🥸 : 🏦 -> 🍌', async () => { const borrowAmount = 69n; await lendingSim.progressSlots(SLOT_JUMP, dateProvider, aztecNode); @@ -292,6 +308,8 @@ describe('e2e_lending_contract', () => { lendingSim.borrow(await lendingAccount.key(), lendingAccount.address, borrowAmount); }); + // Advances slots, calls borrow_public using the lendingAccount public address, and updates + // simulator state. it('Borrow: 🏦 -> 🍌', async () => { const borrowAmount = 69n; await lendingSim.progressSlots(SLOT_JUMP, dateProvider, aztecNode); @@ -311,7 +329,10 @@ describe('e2e_lending_contract', () => { }); }); + // Covers private and public repayment paths back to the lending pool. describe('Repay', () => { + // Creates a burn_private authwit, advances slots, calls repay_private for private debt, and + // updates simulator. it('Repay 🥸 : 🍌 -> 🏦', async () => { const repayAmount = 20n; const authwitNonce = Fr.random(); @@ -336,6 +357,8 @@ describe('e2e_lending_contract', () => { lendingSim.repayPrivate(lendingAccount.address, await lendingAccount.key(), repayAmount); }); + // Creates a burn_private authwit, calls repay_private on behalf of the public account, and + // updates simulator for the public debt path. it('Repay 🥸 on behalf of public: 🍌 -> 🏦', async () => { const repayAmount = 21n; const authwitNonce = Fr.random(); @@ -367,6 +390,8 @@ describe('e2e_lending_contract', () => { lendingSim.repayPrivate(lendingAccount.address, lendingAccount.address.toField(), repayAmount); }); + // Sets a public authwit for burn_public, advances slots, calls repay_public, and updates + // simulator. it('Repay: 🍌 -> 🏦', async () => { const repayAmount = 20n; const authwitNonce = Fr.random(); @@ -399,7 +424,9 @@ describe('e2e_lending_contract', () => { }); }); + // Covers public and private withdrawal paths and over-withdrawal failure. describe('Withdraw', () => { + // Advances slots, calls withdraw_public to retrieve public collateral, and updates simulator. it('Withdraw: 🏦 -> 💰', async () => { const withdrawAmount = 42n; await lendingSim.progressSlots(SLOT_JUMP, dateProvider, aztecNode); @@ -418,6 +445,8 @@ describe('e2e_lending_contract', () => { lendingSim.withdraw(lendingAccount.address.toField(), lendingAccount.address, withdrawAmount); }); + // Advances slots, calls withdraw_private to retrieve private collateral using secret, and + // updates simulator. it('Withdraw 🥸 : 🏦 -> 💰', async () => { const withdrawAmount = 42n; await lendingSim.progressSlots(SLOT_JUMP, dateProvider, aztecNode); @@ -436,7 +465,9 @@ describe('e2e_lending_contract', () => { lendingSim.withdraw(await lendingAccount.key(), lendingAccount.address, withdrawAmount); }); + // Negative path: attempting to withdraw more collateral than available must revert. describe('failure cases', () => { + // Simulates withdraw_public with an impossibly large amount and expects a revert. it('withdraw more than possible to revert', async () => { // Withdraw more than possible to test the revert. logger.info('Withdraw: trying to withdraw more than possible'); diff --git a/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts b/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts index 5e39ebd6949f..5015a2ec076d 100644 --- a/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts +++ b/yarn-project/end-to-end/src/e2e_mempool_limit.test.ts @@ -9,6 +9,8 @@ import { type EndToEndContext, setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; import { proveInteraction } from './test-wallet/utils.js'; +// Verifies that the node rejects incoming transactions when the mempool is at capacity. Uses a +// single automine node with aztecNodeAdmin access; sequencer is paused to let txs accumulate. describe('e2e_mempool_limit', () => { let wallet: TestWallet; let defaultAccountAddress: AztecAddress; @@ -41,6 +43,8 @@ describe('e2e_mempool_limit', () => { afterAll(() => teardown()); + // Sets maxPendingTxCount=2, pauses the sequencer, submits 3 proven txs in order, and asserts + // the first two are accepted (status PENDING) while the third is rejected with LOW_PRIORITY_FEE. it('should evict txs if there are too many', async () => { const tx1 = await proveInteraction( wallet, 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 4254ed127b47..ae0909d4f23b 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 @@ -41,6 +41,13 @@ const createPublisherKeysAndAddresses = () => { }); }; +// Covers the multi-EOA publisher rotation mechanism in the production sequencer. Uses +// PIPELINING_SETUP_OPTS (prod seq, ethereumSlotDuration=4s, aztecSlotDuration=12s, minTxsPerBlock=0) +// with NUM_PUBLISHERS=4 sequencer publisher keys. Tests that when one publisher's L1 tx is +// intercepted (never lands on chain), the sequencer rotates to a different publisher. (v5: the test no +// longer sorts publishers by balance or pins which one is used; it blocks the first publisher attempted +// and asserts a different one takes over. Initializerless accounts deploy nothing at setup, so the +// beforeAll sends a couple of txs to get blocks published across rotated publishers first.) describe('e2e_multi_eoa', () => { jest.setTimeout(5 * 60 * 1000); // 5 minutes @@ -59,6 +66,8 @@ describe('e2e_multi_eoa', () => { jest.restoreAllMocks(); }); + // Exercises publisher rotation: mocks sendRawTransaction to block transactions from the first + // publisher attempted, then verifies a different fallback publisher takes over and the L2 tx is mined. describe('multi-txs block', () => { beforeAll(async () => { let sequencerClient: SequencerClient | undefined; @@ -196,6 +205,9 @@ describe('e2e_multi_eoa', () => { spies.forEach(spy => spy.mockRestore()); }; + // Identifies the two highest-balance publisher accounts from L1 balances, calls + // testAccountRotation twice (simulating a first sender being blocked and a second rotation), + // and asserts that the fallback sender actually submitted the mined L1 block tx. it('publishers are rotated by the sequencer', async () => { // We should be at L2 block 2 or later (empty pipelined checkpoints can land between setup // and the first assertion, so accept >=2 rather than pinning to exactly 2). 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 368766eb8f9b..568cf1bfdcaf 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 @@ -33,6 +33,11 @@ const VALIDATOR_COUNT = 5; const COMMITTEE_SIZE = VALIDATOR_COUNT - 2; const PUBLISHER_COUNT = 2; +// Tests that a single AztecNodeService hosting multiple validator keys correctly signs attestations +// and filters signing to only active committee members. One node, 5 validators staked, committee +// size 3. Uses PIPELINING_SETUP_OPTS (ethSlot=4s, aztecSlot=12s) with no gossip (single in-process +// node, no p2p). CI-excluded (commented out in bootstrap.sh test_cmds). Timeout 15 min. +// Time-warp: cheatCodes.rollup.advanceToEpoch for validator set lag. describe('e2e_multi_validator_node', () => { // Each test starts its own multi-validator network and waits for checkpointed L2 transactions. jest.setTimeout(15 * 60 * 1000); @@ -99,6 +104,7 @@ describe('e2e_multi_validator_node', () => { ); // We jump to the next epoch such that the committee can be setup. + // REFACTOR: retryUntil polling getAttesterView should be replaced with a waitForValidatorActive helper await retryUntil( async () => { const view = await rollup.getAttesterView(validatorAddresses[0]); @@ -133,6 +139,9 @@ describe('e2e_multi_validator_node', () => { ownerAddress = accountManager.address; }; + // Deploys an account and contract, then reads the published checkpoint attestations from the + // archiver. Asserts that quorum (≥ 2/3+1) attestations were collected and that all signers + // belong to the staked validator set. it('should build blocks & attest with multiple validator keys', async () => { await deployOwnerAccount(); @@ -163,6 +172,9 @@ describe('e2e_multi_validator_node', () => { expect(signers.every(s => validatorAddresses.includes(s))).toBe(true); }); + // Initiates withdrawal for two validators (reducing effective committee to 3), advances epochs + // past the validator-set lag, then deploys a contract and verifies that attestation signers are + // limited to the active committee (not the withdrawn validators). it('should attest ONLY with the correct validator keys', async () => { const rollupContract1 = getContract({ address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), diff --git a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts index ba3b05b5d82a..d5467db40532 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts @@ -9,6 +9,9 @@ import { deployToken, expectTokenBalance } from './fixtures/token_utils.js'; import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; +// Verifies that the PXE correctly handles multiple Schnorr accounts sharing the same encryption +// key (different signing keys). Checks that note discovery and balance tracking remain accurate +// across three accounts. Uses AUTOMINE_E2E_OPTS with 3 custom accounts sharing one secret. describe('e2e_multiple_accounts_1_enc_key', () => { let wallet: TestWallet; let accounts: AztecAddress[] = []; @@ -81,6 +84,9 @@ describe('e2e_multiple_accounts_1_enc_key', () => { /** * Tests the ability of the Private eXecution Environment (PXE) to handle multiple accounts under the same encryption key. + * + * Executes three sequential private transfers (0→1, 0→2, 1→2) and asserts balance correctness + * after each transfer, verifying that note discovery works across accounts sharing a secret. */ it('spends notes from multiple account under the same encryption key', async () => { const transferAmount1 = 654n; // account 0 -> account 1 diff --git a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts index a68ea4b91c57..1b2267fd17df 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_blobs.test.ts @@ -17,6 +17,10 @@ import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { PIPELINING_SETUP_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Verifies that a block can contain transactions whose combined side effects span multiple EIP-4844 +// blobs. Uses PIPELINING_SETUP_OPTS (prod seq, ethereumSlotDuration=4s, aztecSlotDuration=12s, +// minTxsPerBlock=0) with setConfig({minTxsPerBlock:3}) to pack all txs into one block. Asserts +// that the resulting block encodes into >1 blob and that every side-effect type is represented. describe('e2e_multiple_blobs', () => { let contract: TestContract; let logger: Logger; @@ -27,6 +31,8 @@ describe('e2e_multiple_blobs', () => { let sequencer: Sequencer; let teardown: () => Promise; + // REFACTOR: hand-rolled state-changed on/off subscription with a manual timeout — a + // waitForSequencerState(IDLE, timeout) DSL helper should replace it. function waitForSequencerIdle(timeout = 30000): Promise { if (sequencer.status().state === SequencerState.IDLE) { return Promise.resolve(); @@ -71,6 +77,9 @@ describe('e2e_multiple_blobs', () => { afterAll(() => teardown()); + // Sets minTxsPerBlock=3, sends 3 txs simultaneously (2 contract-class publishes + 1 BatchCall + // with many side effects), waits for them to land in the same block, encodes the block as blob + // data, and asserts numBlobs > 1 and every side-effect type is non-zero. it('includes multiple txs in a block that produces multiple blobs', async () => { // Increase the minimum number of txs per block so that all txs will be mined in the same block. const TX_COUNT = 3; @@ -101,6 +110,8 @@ describe('e2e_multiple_blobs', () => { // Send them simultaneously to be picked up by the sequencer const sendResults = await Promise.all(provenTxs.map(tx => tx.send({ from: defaultAccountAddress, wait: NO_WAIT }))); + // REFACTOR: Promise.all over individual waitForTx calls; a waitForTxs batch helper would + // consolidate this into a single polling loop. // Wait for all to be mined const receipts = await Promise.all( sendResults.map(({ txHash }) => { diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/importer.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/importer.test.ts index abee1fff29d8..c59493f5f156 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/importer.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/importer.test.ts @@ -4,6 +4,9 @@ import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { NestedContractTest } from './nested_contract_test.js'; +// Tests cross-contract calls through the ImportTest contract (which imports functions from Test). +// NestedContractTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, fundSponsoredFPC, skipAccountDeployment }) +// with 1 public-deployed account. ImportTest and Test contracts are deployed fresh per test in beforeEach. describe('e2e_nested_contract manual', () => { const t = new NestedContractTest('manual'); let testContract: TestContract; @@ -24,16 +27,19 @@ describe('e2e_nested_contract manual', () => { await t.teardown(); }); + // Calls importerContract.call_no_args(testContract.address) and awaits inclusion. it('calls a method no arguments', async () => { logger.info(`Calling noargs on importer contract`); await importerContract.methods.call_no_args(testContract.address).send({ from: defaultAccountAddress }); }); + // Calls importerContract.call_public_fn(testContract.address) and awaits inclusion. it('calls a public function', async () => { logger.info(`Calling public_fn on importer contract`); await importerContract.methods.call_public_fn(testContract.address).send({ from: defaultAccountAddress }); }); + // Calls importerContract.pub_call_public_fn(testContract.address) and awaits inclusion. it('calls a public function from a public function', async () => { logger.info(`Calling pub_public_fn on importer contract`); await importerContract.methods.pub_call_public_fn(testContract.address).send({ from: defaultAccountAddress }); diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_call.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_call.test.ts index 46ce281b4387..e16c60a91bb9 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_call.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_call.test.ts @@ -1,6 +1,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { NestedContractTest } from './nested_contract_test.js'; +// Tests a nested private call from ParentContract into ChildContract's value() function. +// NestedContractTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, fundSponsoredFPC, skipAccountDeployment }) +// with 1 public-deployed account. applyManual() deploys Parent and Child contracts in beforeAll. describe('e2e_nested_contract manual', () => { const t = new NestedContractTest('manual'); let { parentContract, childContract, defaultAccountAddress } = t; @@ -15,6 +18,7 @@ describe('e2e_nested_contract manual', () => { await t.teardown(); }); + // Calls parent.entry_point(child.address, child.value.selector()) and awaits inclusion. it('performs nested calls', async () => { await parentContract.methods .entry_point(childContract.address, await childContract.methods.value.selector()) diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_enqueue.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_enqueue.test.ts index 3996780c3895..1e7d8d6714ed 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_enqueue.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/manual_private_enqueue.test.ts @@ -6,6 +6,9 @@ import { ParentContract } from '@aztec/noir-test-contracts.js/Parent'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { NestedContractTest } from './nested_contract_test.js'; +// Tests parent contracts enqueuing public calls on a child contract via various call patterns. +// NestedContractTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, fundSponsoredFPC, skipAccountDeployment }) +// with 1 public-deployed account. Parent and Child are deployed fresh per test in beforeEach. describe('e2e_nested_contract manual_enqueue', () => { const t = new NestedContractTest('manual_enqueue'); let { wallet, parentContract, childContract, defaultAccountAddress, aztecNode } = t; @@ -28,6 +31,7 @@ describe('e2e_nested_contract manual_enqueue', () => { await t.teardown(); }); + // Enqueues one pub_inc_value(42) call via the parent and asserts child storage equals 42. it('enqueues a single public call', async () => { await parentContract.methods .enqueue_call_to_child(childContract.address, await childContract.methods.pub_inc_value.selector(), 42n) @@ -35,6 +39,7 @@ describe('e2e_nested_contract manual_enqueue', () => { expect(await getChildStoredValue(childContract)).toEqual(new Fr(42n)); }); + // Enqueues pub_inc_value(42) twice via enqueue_call_to_child_twice and asserts child storage is 85. it('enqueues multiple public calls', async () => { await parentContract.methods .enqueue_call_to_child_twice(childContract.address, await childContract.methods.pub_inc_value.selector(), 42n) @@ -42,6 +47,7 @@ describe('e2e_nested_contract manual_enqueue', () => { expect(await getChildStoredValue(childContract)).toEqual(new Fr(85n)); }); + // Calls enqueue_call_to_pub_entry_point which enqueues pub_entry_point → pub_inc_value; asserts 42. it('enqueues a public call with nested public calls', async () => { await parentContract.methods .enqueue_call_to_pub_entry_point(childContract.address, await childContract.methods.pub_inc_value.selector(), 42n) @@ -49,6 +55,7 @@ describe('e2e_nested_contract manual_enqueue', () => { expect(await getChildStoredValue(childContract)).toEqual(new Fr(42n)); }); + // Calls enqueue_calls_to_pub_entry_point which enqueues pub_entry_point twice; asserts 85. it('enqueues multiple public calls with nested public calls', async () => { await parentContract.methods .enqueue_calls_to_pub_entry_point( diff --git a/yarn-project/end-to-end/src/e2e_nested_contract/manual_public.test.ts b/yarn-project/end-to-end/src/e2e_nested_contract/manual_public.test.ts index bf3ede62be4b..49e0714e01d4 100644 --- a/yarn-project/end-to-end/src/e2e_nested_contract/manual_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_contract/manual_public.test.ts @@ -7,6 +7,9 @@ import { serializeToBuffer } from '@aztec/foundation/serialize'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { NestedContractTest } from './nested_contract_test.js'; +// Tests public-to-public nested calls and ordering guarantees (public before private enqueue). +// NestedContractTest wraps setup(0, { ...AUTOMINE_E2E_OPTS, fundSponsoredFPC, skipAccountDeployment }) +// with 1 public-deployed account. applyManual() deploys Parent and Child contracts in beforeAll. describe('e2e_nested_contract manual', () => { const t = new NestedContractTest('manual'); let { wallet, parentContract, childContract, defaultAccountAddress, aztecNode } = t; @@ -24,6 +27,7 @@ describe('e2e_nested_contract manual', () => { await t.teardown(); }); + // Calls parent.pub_entry_point(child, pub_get_value, 42) and awaits inclusion. it('performs public nested calls', async () => { await parentContract.methods .pub_entry_point(childContract.address, await childContract.methods.pub_get_value.selector(), 42n) @@ -31,6 +35,7 @@ describe('e2e_nested_contract manual', () => { }); // Regression for https://github.com/AztecProtocol/aztec-packages/issues/640 + // Calls pub_entry_point_twice so pub_inc_value runs twice in one tx; asserts storage is 84 (not 42). it('reads fresh value after write within the same tx', async () => { await parentContract.methods .pub_entry_point_twice(childContract.address, await childContract.methods.pub_inc_value.selector(), 42n) @@ -42,6 +47,7 @@ describe('e2e_nested_contract manual', () => { // Executes a public call first and then a private call (which enqueues another public call) // through the account contract, if the account entrypoint behaves properly, it will honor // this order and not run the private call first which results in the public calls being inverted. + // Batches pub_set_value(20) and parent.enqueue(pub_set_value(40)); reads public logs to assert [20, 40]. it('executes public calls in expected order', async () => { const pubSetValueSelector = await childContract.methods.pub_set_value.selector(); const actions = [ diff --git a/yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts b/yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts index 3ef3c4e29a8c..bba5474aff81 100644 --- a/yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts +++ b/yarn-project/end-to-end/src/e2e_nested_utility_calls.test.ts @@ -15,6 +15,7 @@ const TIMEOUT = 300_000; // Verifies nested utility calls via pow_utility(x, n) = x^n (recursive utility→utility), // calling it from a private function via pow_private, and the default hook behavior. +// Single automine node, one funded account, two NestedUtilityContract instances. describe('Nested utility calls', () => { let contractA: NestedUtilityContract; let contractB: NestedUtilityContract; @@ -36,27 +37,35 @@ describe('Nested utility calls', () => { afterAll(() => teardown()); + // Simulates pow_utility(2, 0) from the same contract; expects result == 1 with no recursion. it('pow_utility(x, 0) returns 1 (base case, no nested call)', async () => { const { result } = await contractA.methods.pow_utility(2n, 0).simulate({ from: defaultAccountAddress }); expect(result).toEqual(1n); }); + // Simulates pow_utility(2, 10) which recurses 10 times within the same contract; expects 1024. it('pow_utility(2, 10) returns 2^10 (10 levels of nesting)', async () => { const { result } = await contractA.methods.pow_utility(2n, 10).simulate({ from: defaultAccountAddress }); expect(result).toEqual(2n ** 10n); }); + // Simulates pow_private(2, 10) which calls pow_utility from a private function context; expects + // 1024. it('pow_private(2, 10) returns 2^10 (private function calling utility)', async () => { const { result } = await contractA.methods.pow_private(2n, 10).simulate({ from: defaultAccountAddress }); expect(result).toEqual(2n ** 10n); }); + // Simulates contractA.delegate_pow_utility(contractB, 2, 3) with no hook registered; expects + // 'Cross-contract utility call denied'. it('denies cross-contract utility call from utility context by default', async () => { await expect( contractA.methods.delegate_pow_utility(contractB.address, 2n, 3n).simulate({ from: defaultAccountAddress }), ).rejects.toThrow('Cross-contract utility call denied'); }); + // Simulates contractA.delegate_pow_private(contractB, 2, 3) with no hook; expects 'Cross-contract + // utility call denied'. it('denies cross-contract utility call from private function by default', async () => { await expect( contractA.methods.delegate_pow_private(contractB.address, 2n, 3n).simulate({ from: defaultAccountAddress }), @@ -74,6 +83,9 @@ describe('Nested utility calls', () => { }); }); +// Covers the authorizeUtilityCall PXE hook: verifies that the hook is invoked for cross-contract +// utility calls and that its boolean return controls access. Also tests note sync for the target +// contract before the call. Single automine node with a custom hook registered at setup time. describe('authorizeUtilityCall hook', () => { let contractA: NestedUtilityContract; let contractB: NestedUtilityContract; @@ -115,6 +127,7 @@ describe('authorizeUtilityCall hook', () => { lastRequest = undefined; }); + // Calls delegate_pow_utility with hookAllows=false; expects denial and checks lastRequest fields. it('denies cross-contract utility call from utility context when hook returns false', async () => { await expect( contractA.methods.delegate_pow_utility(contractB.address, 2n, 3n).simulate({ from: defaultAccountAddress }), @@ -130,6 +143,7 @@ describe('authorizeUtilityCall hook', () => { }); }); + // Sets hookAllows=true, calls delegate_pow_utility, and asserts result=8 and lastRequest fields. it('allows cross-contract utility call from utility context when hook returns true', async () => { hookAllows = true; const { result } = await contractA.methods @@ -155,6 +169,8 @@ describe('authorizeUtilityCall hook', () => { expect(result).toEqual(contractA.address); }); + // Calls delegate_pow_private with hookAllows=false; expects denial and checks lastRequest + // callerContext is 'private'. it('denies cross-contract utility call from private function when hook returns false', async () => { await expect( contractA.methods.delegate_pow_private(contractB.address, 2n, 3n).simulate({ from: defaultAccountAddress }), @@ -170,6 +186,7 @@ describe('authorizeUtilityCall hook', () => { }); }); + // Sets hookAllows=true, calls delegate_pow_private, and asserts result=8 with 'private' context. it('allows cross-contract utility call from private function when hook returns true', async () => { hookAllows = true; const { result } = await contractA.methods @@ -187,6 +204,7 @@ describe('authorizeUtilityCall hook', () => { }); }); + // Calls delegate_pow_view with hookAllows=false; expects denial with 'private view' context. it('denies cross-contract utility call from view function when hook returns false', async () => { await expect( contractA.methods.delegate_pow_view(contractB.address, 2n, 3n).simulate({ from: defaultAccountAddress }), @@ -202,6 +220,7 @@ describe('authorizeUtilityCall hook', () => { }); }); + // Sets hookAllows=true, calls delegate_pow_view, and asserts result=8 with 'private view' context. it('allows cross-contract utility call from view function when hook returns true', async () => { hookAllows = true; const { result } = await contractA.methods @@ -219,6 +238,9 @@ describe('authorizeUtilityCall hook', () => { }); }); + // Stores pow args as notes on contractB, then calls delegate_pow_from_storage from contractA + // (cross-contract). Asserts that contractB's notes are synced before the utility call so that + // the stored values are discoverable. it('syncs target contract notes on cross-contract utility call', async () => { hookAllows = true; diff --git a/yarn-project/end-to-end/src/e2e_nft.test.ts b/yarn-project/end-to-end/src/e2e_nft.test.ts index 61ed76a31318..04bd19842852 100644 --- a/yarn-project/end-to-end/src/e2e_nft.test.ts +++ b/yarn-project/end-to-end/src/e2e_nft.test.ts @@ -12,6 +12,8 @@ const TIMEOUT = 300_000; // This is a very simple test checking only the happy path. More complete tests of the NFT are implemented with TXE. // This test is only kept around to check that public data writes are squashed as expected. +// Single automine node, four funded accounts (admin, minter, user1, user2), NFTContract deployed. +// Tests are sequential: each depends on the state left by the previous. describe('NFT', () => { jest.setTimeout(TIMEOUT); @@ -42,6 +44,7 @@ describe('NFT', () => { afterAll(() => teardown()); // NOTE: This test is sequential and each test case depends on the previous one + // Calls set_minter on the NFT contract and verifies is_minter returns true for minterAddress. it('sets minter', async () => { await nftContract.methods.set_minter(minterAddress, true).send({ from: adminAddress }); const { result: isMinterAMinter } = await nftContract.methods @@ -50,12 +53,15 @@ describe('NFT', () => { expect(isMinterAMinter).toBe(true); }); + // Mints TOKEN_ID to user1 and checks owner_of returns user1Address. it('minter mints to a user', async () => { await nftContract.methods.mint(user1Address, TOKEN_ID).send({ from: minterAddress }); const { result: ownerAfterMint } = await nftContract.methods.owner_of(TOKEN_ID).simulate({ from: user1Address }); expect(ownerAfterMint).toEqual(user1Address); }); + // Transfers TOKEN_ID from public to private (recipient=user2); asserts public owner becomes + // AztecAddress.ZERO after the shield. it('transfers to private', async () => { // In a simple "shield" flow the sender and recipient are the same. In the "AMM swap to private" flow // the sender would be the AMM contract. @@ -66,6 +72,8 @@ describe('NFT', () => { expect(publicOwnerAfter).toEqual(AztecAddress.ZERO); }); + // Transfers TOKEN_ID from user2 to user1 in private; verifies user1's private NFT list and + // user2's list is empty. it('transfers in private', async () => { await nftContract.methods.transfer_in_private(user2Address, user1Address, TOKEN_ID, 0).send({ from: user2Address }); @@ -76,6 +84,8 @@ describe('NFT', () => { expect(user2Nfts).toEqual([]); }); + // Transfers TOKEN_ID from user1's private balance back to public (recipient=user2); asserts + // public owner is user2. it('transfers to public', async () => { await nftContract.methods.transfer_to_public(user1Address, user2Address, TOKEN_ID, 0).send({ from: user1Address }); @@ -83,6 +93,7 @@ describe('NFT', () => { expect(publicOwnerAfter).toEqual(user2Address); }); + // Transfers TOKEN_ID in public from user2 to user1 and asserts the public owner changes. it('transfers in public', async () => { await nftContract.methods.transfer_in_public(user2Address, user1Address, TOKEN_ID, 0).send({ from: user2Address }); diff --git a/yarn-project/end-to-end/src/e2e_note_getter.test.ts b/yarn-project/end-to-end/src/e2e_note_getter.test.ts index db658379f610..c05f07f35560 100644 --- a/yarn-project/end-to-end/src/e2e_note_getter.test.ts +++ b/yarn-project/end-to-end/src/e2e_note_getter.test.ts @@ -16,6 +16,9 @@ function boundedVecToArray(boundedVec: NoirBoundedVec): T[] { return boundedVec.storage.slice(0, Number(boundedVec.len)); } +// Covers the NoteGetter contract's filtering capabilities (EQ, NEQ, LT, GT, LTE, GTE comparators +// and sub-field property selectors) and the TestContract's note status filter (active vs nullified). +// Single automine node, one funded account, contracts deployed per describe block. describe('e2e_note_getter', () => { let wallet: Wallet; let defaultAddress: AztecAddress; @@ -31,6 +34,8 @@ describe('e2e_note_getter', () => { afterAll(() => teardown()); + // Verifies all six Comparator variants (EQ, NEQ, LT, GT, LTE, GTE) against a set of notes with + // values 0-9 plus a duplicate 5. describe('comparators', () => { let contract: NoteGetterContract; @@ -38,6 +43,8 @@ describe('e2e_note_getter', () => { ({ contract } = await NoteGetterContract.deploy(wallet).send({ from: defaultAddress })); }); + // Inserts 10 notes (0-9) plus a duplicate 5. Runs all 6 comparator queries in parallel and + // asserts each returns the expected set of values. it('inserts notes from 0-9, then makes multiple queries specifying the total suite of comparators', async () => { await Promise.all( Array(10) @@ -78,6 +85,8 @@ describe('e2e_note_getter', () => { }); }); + // Verifies that the sub-field property selector correctly extracts individual u8 sub-values + // packed into a single Field (using LSB offset/length convention). describe('sub-field property selector', () => { let contract: NoteGetterContract; @@ -95,6 +104,7 @@ describe('e2e_note_getter', () => { ]); }); + // Queries notes where high==1 (offset=1, length=1) and expects [(1,10),(1,20)]. it('filters by high sub-field', async () => { // high occupies offset=1, length=1 in the packed Field (second LSB) const { result } = await contract.methods @@ -111,6 +121,7 @@ describe('e2e_note_getter', () => { ); }); + // Queries notes where low==10 (offset=0, length=1) and expects [(1,10),(2,10)]. it('filters by low sub-field', async () => { // low occupies offset=0, length=1 in the packed Field (LSB) const { result } = await contract.methods @@ -127,6 +138,7 @@ describe('e2e_note_getter', () => { ); }); + // Queries notes where low>10 and expects [(1,20),(3,30)]. it('filters with GT comparator on sub-field', async () => { // low > 10 should match (1,20) and (3,30) const { result } = await contract.methods @@ -144,6 +156,8 @@ describe('e2e_note_getter', () => { }); }); + // Verifies the NoteStatus filter: activeOrNullified=false returns only live notes; =true returns + // both active and nullified notes. describe('status filter', () => { let contract: TestContract; let owner: AztecAddress; @@ -188,14 +202,17 @@ describe('e2e_note_getter', () => { ).rejects.toThrow(expectedError); } + // Note filter with activeOrNullified=false: only live notes are visible. describe('active note only', () => { const activeOrNullified = false; + // Creates a note and asserts it is returned by both call_view_notes and call_get_notes. it('returns active notes', async () => { await contract.methods.call_create_note(VALUE, owner, storageSlot, makeTxHybrid).send({ from: defaultAddress }); await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); }); + // Creates then destroys a note; expects both note-query methods to throw (no live note). it('does not return nullified notes', async () => { await contract.methods.call_create_note(VALUE, owner, storageSlot, makeTxHybrid).send({ from: defaultAddress }); await contract.methods.call_destroy_note(owner, storageSlot).send({ from: defaultAddress }); @@ -204,14 +221,18 @@ describe('e2e_note_getter', () => { }); }); + // Note filter with activeOrNullified=true: both live and nullified notes are returned. describe('active and nullified notes', () => { const activeOrNullified = true; + // Creates a note and asserts it is returned when including nullified notes. it('returns active notes', async () => { await contract.methods.call_create_note(VALUE, owner, storageSlot, makeTxHybrid).send({ from: defaultAddress }); await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); }); + // Creates then destroys a note; asserts that the nullified note is still returned with + // activeOrNullified=true. it('returns nullified notes', async () => { await contract.methods.call_create_note(VALUE, owner, storageSlot, makeTxHybrid).send({ from: defaultAddress }); await contract.methods.call_destroy_note(owner, storageSlot).send({ from: defaultAddress }); @@ -219,6 +240,8 @@ describe('e2e_note_getter', () => { await assertNoteIsReturned(storageSlot, VALUE, activeOrNullified); }); + // Creates two notes at the same slot, destroys one; asserts that call_view_notes_many and + // call_get_notes_many both return exactly two values (both active and nullified). it('returns both active and nullified notes', async () => { // We store two notes with two different values in the same storage slot, and then delete one of them. Note that // we can't be sure which one was deleted since we're just deleting based on the storage slot. diff --git a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts index a44255b1e78e..1fbaee2d6ae2 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_effect.test.ts @@ -12,6 +12,10 @@ import { proveInteraction } from './test-wallet/utils.js'; const TIMEOUT = 300_000; +// Covers the offchain-effect mechanism: effects returned from send(), effects returned from +// proveInteraction, and the offchain-message delivery flow (emitting an event or note as an +// offchain message, then delivering it via offchain_receive and retrieving via getPrivateEvents). +// Single automine node, one funded account, two OffchainEffectContract instances. describe('e2e_offchain_effect', () => { let contract1: OffchainEffectContract; let contract2: OffchainEffectContract; @@ -33,6 +37,8 @@ describe('e2e_offchain_effect', () => { afterAll(() => teardown()); + // Sends emit_offchain_effects with 2 effects; asserts the returned offchainEffects array has + // length 2, that effects are reversed (popped from BoundedVec end), and contractAddresses match. it('should return offchain effects from send()', async () => { const effects = Array(2) .fill(null) @@ -55,6 +61,8 @@ describe('e2e_offchain_effect', () => { expect(offchainEffects[1].data).toEqual(effects[0].data); }); + // Proves emit_offchain_effects with 3 effects via proveInteraction; asserts that + // provenTx.offchainEffects matches the expected reversed order with correct contractAddresses. it('should emit offchain effects', async () => { const effects = Array(3) .fill(null) @@ -80,6 +88,7 @@ describe('e2e_offchain_effect', () => { expect(provenTx.offchainEffects).toEqual(expectedOffchainEffects); }); + // Proves emit_offchain_effects with empty input; asserts provenTx.offchainEffects is empty. it('should not emit any offchain effects', async () => { const provenTx = await proveInteraction(wallet, contract1.methods.emit_offchain_effects([]), { from: defaultAccountAddress, @@ -87,6 +96,8 @@ describe('e2e_offchain_effect', () => { expect(provenTx.offchainEffects).toEqual([]); }); + // Sends emit_event_as_offchain_message_for_msg_sender, captures the offchain message, delivers + // it via offchain_receive (simulated), and retrieves the event from PXE via getPrivateEvents. it('should emit event as offchain message and process it', async () => { const [a, b, c] = [1n, 2n, 3n]; const recipient = defaultAccountAddress; @@ -136,6 +147,8 @@ describe('e2e_offchain_effect', () => { }); }); + // Sends emit_note_as_offchain_message, delivers it via offchain_receive, and reads the note + // value back via get_note_value to verify the note was properly committed. it('should emit note as offchain message and process it', async () => { const value = 123n; const owner = defaultAccountAddress; diff --git a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts index c1158d3a998a..9ed7ae2ceeac 100644 --- a/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts +++ b/yarn-project/end-to-end/src/e2e_offchain_payment.test.ts @@ -15,6 +15,9 @@ import { proveInteraction } from './test-wallet/utils.js'; const TIMEOUT = 300_000; +// Tests the OffchainPayment contract's offchain message delivery mechanism. Uses a single node with the +// AutomineSequencer so each tx mines a block immediately. Also exercises the AutomineSequencer's +// revertToCheckpoint to simulate a reorg and verify that offchain-delivered notes are reprocessed correctly. describe('e2e_offchain_payment', () => { let contract: OffchainPaymentContract; let aztecNode: AztecNode; @@ -52,6 +55,8 @@ describe('e2e_offchain_payment', () => { logger.info(`Reverted to checkpoint ${checkpointBeforeTx}`); } + // Mints tokens to Alice on-chain, executes a private transfer that emits offchain messages, delivers + // those messages to Bob and Alice via simulate(), then checks both balances are correct. it('processes an offchain-delivered private payment via QR-style handoff', async () => { const [alice, bob] = accounts; @@ -105,6 +110,9 @@ describe('e2e_offchain_payment', () => { expect(aliceBalance).toBe(mintAmount - paymentAmount); }); + // Proves a private transfer that emits offchain messages, delivers notes, performs a checkpoint + // revert via AutomineSequencer.revertToCheckpoint(), then forces a block re-mine and polls until + // the PXE reprocesses the re-mined offchain effects and restores Bob's balance. it('reprocesses an offchain-delivered payment after an L1 reorg', async () => { const [alice, bob] = accounts; const mintAmount = 100n; @@ -189,6 +197,8 @@ describe('e2e_offchain_payment', () => { // Wait for the PXE to process the re-mined block and update its note view. // The PXE syncs asynchronously from the archiver, so the balance may lag briefly. + // REFACTOR: hand-rolled poll waiting for PXE to reprocess re-mined offchain notes; a DSL helper + // (e.g. waitForNoteBalance or waitForPXESync) should replace this retryUntil loop. await retryUntil( async () => { const { result } = await contract.methods.get_balance(bob).simulate({ from: bob }); diff --git a/yarn-project/end-to-end/src/e2e_option_params.test.ts b/yarn-project/end-to-end/src/e2e_option_params.test.ts index 1ece966388d9..2678e7acdbf7 100644 --- a/yarn-project/end-to-end/src/e2e_option_params.test.ts +++ b/yarn-project/end-to-end/src/e2e_option_params.test.ts @@ -13,6 +13,9 @@ const TIMEOUT = 300_000; const U64_MAX = 2n ** 64n - 1n; const I64_MIN = -(2n ** 63n); +// Verifies that the Aztec.js ABI layer correctly serialises/deserialises Noir Option parameters +// for public, utility, and private functions. Single node with AutomineSequencer; all calls are +// simulate()-only (no on-chain state changes). describe('Option params', () => { let contract: OptionParamContract; let wallet: Wallet; @@ -40,6 +43,8 @@ describe('Option params', () => { afterAll(() => teardown()); + // Simulates a public function accepting Option with undefined, null, and a real value, + // asserting each maps to None / None / Some correctly. it('accepts ergonomic Option params for public functions', async () => { const { result } = await contract.methods .return_public_optional_struct(undefined) @@ -57,6 +62,7 @@ describe('Option params', () => { expect(someResult).toEqual(someValue); }); + // Same Option round-trip check for a Noir utility function via simulate(). it('accepts ergonomic Option params for utility functions', async () => { const { result: undefinedResult } = await contract.methods .return_utility_optional_struct(undefined) @@ -74,6 +80,7 @@ describe('Option params', () => { expect(someResult).toEqual(someValue); }); + // Same Option round-trip check for a Noir private function via simulate(). it('accepts ergonomic Option params for private functions', async () => { const { result: undefinedResult } = await contract.methods .return_private_optional_struct(undefined) diff --git a/yarn-project/end-to-end/src/e2e_orderbook.test.ts b/yarn-project/end-to-end/src/e2e_orderbook.test.ts index e467aa789096..d9b6390ae178 100644 --- a/yarn-project/end-to-end/src/e2e_orderbook.test.ts +++ b/yarn-project/end-to-end/src/e2e_orderbook.test.ts @@ -20,6 +20,10 @@ const TIMEOUT = 300_000; // // We keep this test around because it's the only TS test where we have async completion of a partial note (partial // note created in one tx and completed in another). +// +// Exercises the Orderbook contract's create_order / fulfill_order flow. Uses a single node with AutomineSequencer +// and three accounts (admin, maker, taker). Each step lands in its own block; the partial note opened by +// create_order is completed in fulfill_order's block. describe('Orderbook', () => { jest.setTimeout(TIMEOUT); @@ -64,9 +68,13 @@ describe('Orderbook', () => { afterAll(() => teardown()); + // Happy-path sequence: create an order (opening a partial note), then fulfill it (completing that note + // in the next tx). Two sequential it() blocks intentionally share state through `orderId`. describe('full flow - happy path', () => { let orderId: FieldLike; + // Maker creates an authwit authorising the orderbook to escrow bidAmount of token0, calls + // create_order, then asserts the OrderCreated event was emitted and the orderbook holds bidAmount. it('creates an order', async () => { const nonceForAuthwits = Fr.random(); @@ -117,6 +125,8 @@ describe('Orderbook', () => { }); // Note that this test case depends on the previous one. + // Taker creates an authwit for finalize_transfer_to_private_from_private, calls fulfill_order, + // then asserts the OrderFulfilled event and final private balances for both maker and taker. it('fulfills an order', async () => { const nonceForAuthwits = Fr.random(); diff --git a/yarn-project/end-to-end/src/e2e_ordering.test.ts b/yarn-project/end-to-end/src/e2e_ordering.test.ts index 342d2ae132d5..eb9f0e214cdf 100644 --- a/yarn-project/end-to-end/src/e2e_ordering.test.ts +++ b/yarn-project/end-to-end/src/e2e_ordering.test.ts @@ -1,4 +1,5 @@ // Test suite for testing proper ordering of side effects +// See https://github.com/AztecProtocol/aztec-packages/issues/1601 for motivation. import type { FunctionSelector } from '@aztec/aztec.js/abi'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; @@ -20,6 +21,8 @@ import { proveInteraction } from './test-wallet/utils.js'; const TIMEOUT = 300_000; // See https://github.com/AztecProtocol/aztec-packages/issues/1601 +// Verifies deterministic execution ordering for enqueued public calls and public state updates. +// Uses a single node with AutomineSequencer; each test mines one block per call via beforeEach setup. describe('e2e_ordering', () => { jest.setTimeout(TIMEOUT); @@ -52,6 +55,7 @@ describe('e2e_ordering', () => { afterEach(() => teardown()); + // Sub-suite deploying Parent and Child contracts fresh in each test to ensure isolation. describe('with parent and child contract', () => { let parent: ParentContract; let child: ChildContract; @@ -63,6 +67,8 @@ describe('e2e_ordering', () => { pubSetValueSelector = await child.methods.pub_set_value.selector(); }, TIMEOUT); + // Asserts that enqueued public calls execute in the order they were enqueued (nested-first vs direct-first), + // verified by reading public logs from the mined block in canonical order. describe('enqueued public calls ordering', () => { const nestedValue = 10n; const directValue = 20n; @@ -72,6 +78,9 @@ describe('e2e_ordering', () => { enqueue_calls_to_child_with_nested_last: [directValue, nestedValue] as bigint[], // eslint-disable-line camelcase } as const; + // Proves a parent tx that enqueues two public calls (direct and nested) in different orderings; asserts + // the calldata hashes match, the calls are enqueued in the expected order, and public logs arrive in + // that same order in the mined block. it.each(['enqueue_calls_to_child_with_nested_first', 'enqueue_calls_to_child_with_nested_last'] as const)( 'orders public function execution in %s', async method => { @@ -105,6 +114,8 @@ describe('e2e_ordering', () => { ); }); + // Asserts that public storage writes from multiple nested calls are applied in the expected order + // and that the final persisted value matches the last write in execution order. describe('public state update ordering, and final state value check', () => { const nestedValue = 10n; const directValue = 20n; @@ -115,6 +126,8 @@ describe('e2e_ordering', () => { set_value_with_two_nested_calls: [nestedValue, directValue, directValue, nestedValue, directValue] as bigint[], // eslint-disable-line camelcase } as const; + // Calls each method variant on the child and reads back getPublicStorageAt to confirm the final + // persisted value equals the last write in the expected ordering sequence. it.each([ 'set_value_twice_with_nested_first', 'set_value_twice_with_nested_last', @@ -128,6 +141,8 @@ describe('e2e_ordering', () => { expect(value.toBigInt()).toBe(expectedOrder[expectedOrder.length - 1]); // final state should match last value set }); + // Calls each method variant and reads the block's public logs via getBlock; asserts they arrive in + // the same order as the expected write sequence. it.each([ 'set_value_twice_with_nested_first', 'set_value_twice_with_nested_last', diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index b19bdf3e89c2..c677bbe2f9a4 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -61,6 +61,12 @@ jest.setTimeout(1000 * 60 * 20); * The sequencers proposer a proposal, the proposal is executed and the new rollup is added to the registry * The nodes are then updated to use the new rollup and we send transactions to try cross-chain in both directions * ensuring that it also works on the new rollup. + * + * Setup: P2PNetworkTest (real libp2p, 4 validator nodes + 1 prover node). SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES + * (ethSlot=4s, aztecSlot=12s, proofSubEpochs=640) with governanceProposerRoundSize=10, minTxsPerBlock=0, + * inboxLag=2. Full governance upgrade flow: validators signal a new rollup payload over real p2p, governance + * vote executes, nodes migrate to the new rollup. Exercises L1→L2 (Inbox) and L2→L1 (Outbox) bridging on + * both the old and new rollup. */ describe('e2e_p2p_add_rollup', () => { let t: P2PNetworkTest; @@ -107,6 +113,9 @@ describe('e2e_p2p_add_rollup', () => { } }); + // Runs the full upgrade lifecycle: deploy a new rollup, have validators signal it over real p2p gossip, + // reach governance quorum, execute the proposal, migrate all validator nodes to the new rollup, then + // verify L1↔L2 cross-chain messaging works on both the old and new rollup. it('Should cast votes to add new rollup to registry', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { @@ -241,6 +250,7 @@ describe('e2e_p2p_add_rollup', () => { await waitL1Block(); t.logger.info('Creating nodes'); + // REFACTOR: sleep(4000) below is a hand-rolled connectivity wait; replace with t.waitForP2PMeshConnectivity nodes = await createNodes( { ...t.ctx.aztecNodeConfig, governanceProposerPayload: newPayloadAddress }, t.ctx.dateProvider, @@ -436,6 +446,7 @@ describe('e2e_p2p_add_rollup', () => { t.ctx.aztecNodeConfig.l1RpcUrls, ); + // REFACTOR: while(true) polling loop with sleep is hand-rolled; replace with retryUntil let govData; while (true) { govData = await govInfo(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts index daf0d16c481e..21a93d7082c2 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts @@ -40,6 +40,11 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'broadcasted-invalid-bloc * 4. Wait for the committee to be formed * 5. Send a transaction that will trigger a block proposal * 6. Expect that the invalid proposer gets slashed + * + * Setup: P2PNetworkTest with mockGossipSubNetwork:true (in-memory bus, NOT real libp2p). 4 validators, + * ethSlot=4s, aztecSlot=8s, epoch=2, proofSubEpochs=1024 (no pruning), minTxsPerBlock=0, inboxLag=2. + * Uses P2PNetworkTest only for L1/validator-registration harness; transport is mock gossip. + * Candidate for relocation to e2e_slashing/. */ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => { let t: P2PNetworkTest; @@ -94,6 +99,10 @@ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => { await t.ctx.cheatCodes.rollup.debugRollup(); }; + // Verifies the BROADCASTED_INVALID_BLOCK_PROPOSAL slash path: one node sends bad block proposals while + // honest nodes detect the offense, collect it across the committee, and trigger an on-chain slash. + // The test waits for P2P mesh formation, finds a slot where the malicious node is proposer, then + // confirms the slash amount and attester address are recorded on L1. it('slashes validator who broadcasts invalid block proposal', async () => { const { rollup } = await t.getContracts(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts index 894226a7e280..c83b835d4454 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts @@ -50,6 +50,10 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'data-withholding-slash-' * a slot-keyed DATA_WITHHOLDING for the three attesters (A, B, C). * 8. With slashSelfAllowed the offense reaches quorum; A, B, C are slashed on L1. D is * not slashed because it never attested. + * + * Setup: P2PNetworkTest with real libp2p (no mockGossipSubNetwork). 4 validators, ethSlot=4s, + * aztecSlot=12s, epoch=2, proofSubEpochs=1024, minTxsPerBlock=1, inboxLag=2, slashSelfAllowed. + * Uses jest.spyOn to suppress tx gossip and stub proposal handlers on specific nodes. */ describe('e2e_p2p_data_withholding_slash', () => { let t: P2PNetworkTest; @@ -108,6 +112,10 @@ describe('e2e_p2p_data_withholding_slash', () => { } }); + // Configures a 4-node real-libp2p network with a malicious proposer (tx gossip suppressed), two blind + // attesters (accept any proposal without re-execution), and one honest node that refuses to attest. After + // the tolerance window the watchers detect DATA_WITHHOLDING offenses for the three attesters (A, B, C) + // and assert only D — the honest non-attester — is not slashed. it('slashes attesters that attest to proposals containing withheld transactions', async () => { if (!t.bootstrapNodeEnr) { throw new Error('Bootstrap node ENR is not available'); diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts index 5e394b33192e..605bd8027e21 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts @@ -49,6 +49,11 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'duplicate-attestation-sl * NOTE: This test triggers BOTH duplicate proposal (from malicious proposers sharing a key) AND duplicate attestation * (from the malicious proposers attesting to multiple proposals). We verify specifically that the duplicate * attestation offense is recorded. + * + * Setup: P2PNetworkTest with mockGossipSubNetwork:true (in-memory bus, NOT real libp2p). 4 validators, + * ethSlot=8s, aztecSlot=24s, epoch=2, proofSubEpochs=1024, minTxsPerBlock=0, inboxLag=2 (v5 always enforces + * the timetable, so the former enforceTimeTable/l1PublishingTime overrides are gone). + * Candidate for relocation to e2e_slashing/. */ describe('e2e_p2p_duplicate_attestation_slash', () => { let t: P2PNetworkTest; @@ -107,6 +112,10 @@ describe('e2e_p2p_duplicate_attestation_slash', () => { await t.ctx.cheatCodes.rollup.debugRollup(); }; + // Two malicious nodes share a validator key and both attest to each other's proposals + // (attestToEquivocatedProposals:true). Honest nodes detect the DUPLICATE_ATTESTATION offense and verify + // the offending attester is the shared key's address. Also exercises DUPLICATE_PROPOSAL as a side effect + // but asserts specifically that DUPLICATE_ATTESTATION is recorded. it('slashes validator who sends duplicate attestations', async () => { const { rollup } = await t.getContracts(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts index 07f778961cd7..82675b476581 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts @@ -42,6 +42,11 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'duplicate-proposal-slash * 2. The two nodes with the same key will both detect they are proposers for the same slot and naturally race to propose * 3. Since they have different coinbase addresses, their proposals will have different archives (different content) * 4. Other validators will detect the duplicate and emit a slash event + * + * Setup: P2PNetworkTest with mockGossipSubNetwork:true (in-memory bus, NOT real libp2p). 4 validators, + * ethSlot=8s, aztecSlot=24s, epoch=2, proofSubEpochs=1024, minTxsPerBlock=0, inboxLag=2 (v5 always enforces + * the timetable, so the former enforceTimeTable override is gone). + * Candidate for relocation to e2e_slashing/. */ describe('e2e_p2p_duplicate_proposal_slash', () => { let t: P2PNetworkTest; @@ -99,6 +104,11 @@ describe('e2e_p2p_duplicate_proposal_slash', () => { await t.ctx.cheatCodes.rollup.debugRollup(); }; + // Two malicious nodes share a validator key but have different coinbase addresses so their proposals + // differ. Honest nodes receive both proposals via mock gossip, detect the equivocation, and record a + // DUPLICATE_PROPOSAL offense. The test collects offenses from all nodes (equivocation may only be + // observed by whichever node processed both proposals before the slot closed) and asserts the offense + // is attributed to the shared key's address. it('slashes validator who sends duplicate proposals', async () => { const { rollup } = await t.getContracts(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/fee_asset_price_oracle_gossip.test.ts b/yarn-project/end-to-end/src/e2e_p2p/fee_asset_price_oracle_gossip.test.ts index ded237e78e6b..e3da2ec43a58 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/fee_asset_price_oracle_gossip.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/fee_asset_price_oracle_gossip.test.ts @@ -43,6 +43,10 @@ const qosAlerts: AlertConfig[] = [ }, ]; +// Tests that the fee-asset price oracle value set on a mock L1 StateView contract gossips through the +// real libp2p validator network and converges on the rollup's on-chain price. Uses P2PNetworkTest with +// SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=24s, epoch=4, proofSubEpochs=640) plus a +// real prover node. CHECK_ALERTS env var gates optional Grafana alert validation. describe('e2e_p2p_network', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -89,6 +93,10 @@ describe('e2e_p2p_network', () => { } }); + // Deploys a MockStateView L1 contract, sets an initial oracle price, then starts 4 validator nodes + // and a prover. Adjusts the oracle price twice and uses retryUntil to confirm the rollup's on-chain + // price converges to each target within the gossip propagation window. + // REFACTOR: sleep(8000) for peer discovery is hand-rolled; replace with t.waitForP2PMeshConnectivity it('should rollup txs from all peers', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index 305914f58b31..dad00a41857e 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -49,6 +49,11 @@ const qosAlerts: AlertConfig[] = [ }, ]; +// Tests end-to-end gossip propagation with 4 validators, a fake prover node, and a non-validator +// monitoring node (alwaysReexecuteBlockProposals:true). Uses P2PNetworkTest with real libp2p, +// SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=36s, epoch=4, proofSubEpochs=640), +// inboxLag=2. Asserts txs are mined from all nodes, attestation signers match the validator set, +// and the prover node produces a proven block by collecting txs from p2p. describe('e2e_p2p_network', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -96,6 +101,10 @@ describe('e2e_p2p_network', () => { } }); + // Stands up 4 validators + 1 prover + 1 re-execution monitor, submits 2 txs per node, and waits + // for all txs to mine. Checks attestation signers match the validator set and confirms the prover + // eventually produces a proven block (collecting txs from p2p rather than RPC). + // REFACTOR: Promise.all over waitForTx calls is hand-rolled; extract to a shared helper it('should rollup txs from all peers', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { 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 28a463778a31..f78bcccfa651 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 @@ -50,6 +50,10 @@ const qosAlerts: AlertConfig[] = [ }, ]; +// Tests gossip propagation using the real CLI validator-registration path (addL1Validator + ZkPassport mock) +// instead of the MultiAdder cheat shortcut used by applyBaseSetup. Uses P2PNetworkTest with +// SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=24s, proofSubEpochs=640) and real libp2p. +// Distinct from gossip_network.test.ts specifically because of the validator-registration flow. describe('e2e_p2p_network', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -93,6 +97,9 @@ describe('e2e_p2p_network', () => { } }); + // Registers validators via the real addL1Validator CLI path (with ZkPassport mock proof), submits + // transactions to each node, and asserts all txs are mined and that attestation signers match + // the registered validator set. Validates the non-cheat registration flow end-to-end. it('should rollup txs from all peers (and add the validators without cheating)', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts index 9be090078fd6..303dd9bc00d8 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts @@ -10,6 +10,10 @@ jest.setTimeout(1000 * 60 * 10); const SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD = 1; +// Verifies the basic inactivity slash path: one of 6 validators has its sequencer stopped; after +// slashInactivityConsecutiveEpochThreshold=1 epoch of inactivity the sentinel detects the offense and +// the validator is slashed on L1. Uses P2PInactivityTest (real libp2p, 6 nodes, fake prover, ethSlot +// varies by CI env, epoch=2, proofSubEpochs=1024, sentinelEnabled). describe('e2e_p2p_inactivity_slash', () => { let test: P2PInactivityTest; @@ -25,6 +29,8 @@ describe('e2e_p2p_inactivity_slash', () => { await test?.teardown(); }); + // Waits for a Slash event on the rollup contract and asserts it targets the offline validator with + // the expected slashing amount. Simple event-driven assertion; no polling inside the test body. it('slashes inactive validator', async () => { const slashPromise = promiseWithResolvers<{ amount: bigint; attester: EthAddress }>(); test.rollup.listenToSlash(args => { diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts index 78af77bc07d6..1dc0c0f72e72 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash_with_consecutive_epochs.test.ts @@ -12,6 +12,9 @@ import { P2PInactivityTest } from './inactivity_slash_test.js'; jest.setTimeout(1000 * 60 * 10); +// Verifies that consecutive-epoch threshold is respected: 2 validators are offline, but one is re-enabled +// after the first epoch. Only the permanently-offline validator should be slashed. Uses P2PInactivityTest +// (real libp2p, 6 nodes, fake prover, epoch=2, proofSubEpochs=1024, threshold=3 consecutive epochs). describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { let test: P2PInactivityTest; @@ -28,6 +31,9 @@ describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => { await test?.teardown(); }); + // Re-enables one of the two offline validators after the first epoch, then waits for INACTIVITY + // offenses to appear. Asserts that offenses are only emitted for the permanently-offline validator + // and that the re-enabled validator is never included in the slash. it('only slashes validator inactive for N consecutive epochs', async () => { const [offlineValidator, reenabledValidator] = test.offlineValidators; diff --git a/yarn-project/end-to-end/src/e2e_p2p/late_prover_tx_collection.test.ts b/yarn-project/end-to-end/src/e2e_p2p/late_prover_tx_collection.test.ts index 6f4af64b6a26..a50a3c5d89d5 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/late_prover_tx_collection.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/late_prover_tx_collection.test.ts @@ -31,6 +31,11 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'late-prover-')); jest.setTimeout(1000 * 60 * 10); +// Tests the reqresp BLOCK_TXS path for a prover that joins after a block has already been mined. The +// prover learns the block via L1/archiver sync but never received the proposal or txs via gossip. +// It must fetch the missing txs from peers over reqresp. Setup: P2PNetworkTest real libp2p, 4 validators, +// SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=12s, epoch=4, proofSubEpochs=640), +// minTxsPerBlock=1, inboxLag=2. Late prover node created after transactions are already mined. describe('e2e_p2p_late_prover_tx_collection', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[] = []; @@ -70,6 +75,9 @@ describe('e2e_p2p_late_prover_tx_collection', () => { fs.rmSync(`${DATA_DIR}-late-prover`, { recursive: true, force: true, maxRetries: 3 }); }); + // Mines a block with 2 txs via 4 validators, then starts a prover node late (after gossip has already + // propagated). Waits for the prover to sync the block from L1 and connect to peers, then drives + // txCollection.collectFastForBlock directly and asserts all block txs are collected over reqresp. it("lets a late-joining prover collect a mined block's txs from dumb peers when it has no local proposal", async () => { if (!t.bootstrapNodeEnr) { throw new Error('Bootstrap node ENR is not available'); diff --git a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts index 146828dbf375..33cbad49dca0 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts @@ -27,8 +27,12 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'validators-sentinel-')); jest.setTimeout(1000 * 60 * 10); -// Regression test for sentinel properly detecting attestations of validators -// running on the same node as the proposer who pushed a given block. +// Regression test for the sentinel correctly tracking attestations for multiple validators co-hosted on +// the same physical node as the proposer. Uses P2PNetworkTest with real libp2p: 2 nodes each carrying 3 +// validator keys (6 validators total) plus a non-validator sentinel node and a fake prover. ethSlot=8s, +// aztecSlot=36s, epoch=2, proofSubEpochs=1024, sentinelEnabled (v5 always enforces the timetable, so the +// former enforceTimeTable/l1PublishingTime overrides are gone). Dynamic port via +// getBootNodeUdpPort(). Each it runs as an isolated CI job (parallel convention). // REFACTOR: This test shares much code with `validators_sentinel` so we may be able to refactor common parts out. describe('e2e_p2p_multiple_validators_sentinel', () => { let t: P2PNetworkTest; @@ -120,6 +124,8 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { ); }; + // Waits past the pipelining warm-up period, then observes SLOT_COUNT slots and asserts that every + // validator on every node has zero attestation-missed entries in the sentinel history for those slots. it('collects attestations for all validators on a node', async () => { // Ensure all nodes see each other, especially the sentinel, before starting slot counting await t.waitForP2PMeshConnectivity([...nodes, sentinel]); @@ -161,6 +167,11 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { } }); + // Stops the second validator node mid-run so it can no longer build blocks. Finds a slot where one + // of the first node's validators is the proposer, then queries sentinel stats from the sentinel node + // and asserts: first-node validators have no missed entries for that slot; offline validators have + // missed entries; and at least one first-node validator has a checkpoint-mined or checkpoint-valid + // entry confirming the block was proposed. it('collects attestations for validators in proposer node when block is not published', async () => { // Ensure all nodes see each other, especially the sentinel await t.waitForP2PMeshConnectivity([...nodes, sentinel]); diff --git a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts index 8061bce031fe..86d5ddb6f6b9 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts @@ -33,6 +33,11 @@ const CHECK_ALERTS = process.env.CHECK_ALERTS === 'true'; * The other validators connect to everyone * We check that the submitted transactions are mined and that the block * contains attestations from all validators + * + * Setup: P2PNetworkTest real libp2p, SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=24s, + * epoch=4, proofSubEpochs=640), inboxLag=2, p2pMaxFailedAuthAttemptsAllowed=0. 7 nodes total: 2 regular + * + 2 preferred (p2pAllowOnlyValidators, no discovery) + 2 validators with discovery + 1 validator + * without discovery. No prover. jest.setTimeout=30m. */ // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds @@ -57,6 +62,9 @@ const qosAlerts: AlertConfig[] = [ }, ]; +// Tests the preferred-node (supernode) topology: preferred nodes only accept validator connections; +// a no-discovery validator connects exclusively through preferred nodes; gossip monitors assert that +// traffic flows only through expected peers. Verifies txs mine and attestation signers match validators. describe('e2e_p2p_preferred_network', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -169,6 +177,11 @@ describe('e2e_p2p_preferred_network', () => { } }); + // Creates a 7-node topology (2 regular + 2 preferred + 2 validators + 1 no-discovery validator), + // installs gossip monitors to verify no-discovery validators only receive traffic from preferred nodes, + // submits txs from regular nodes, and asserts all txs mine with attestations from all validators. + // REFACTOR: peer-count polling loop in waitForNodeToAcquirePeers is hand-rolled; consider + // using t.waitForP2PMeshConnectivity with a peer-count predicate it('should rollup txs from all peers', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { diff --git a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts index 36aa8ecc3b15..39f446176746 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/rediscovery.test.ts @@ -1,7 +1,6 @@ import type { AztecNodeService } from '@aztec/aztec-node'; import { waitForTx } from '@aztec/aztec.js/node'; import { TxHash } from '@aztec/aztec.js/tx'; -import { sleep } from '@aztec/foundation/sleep'; import fs from 'fs'; import os from 'os'; @@ -20,6 +19,9 @@ const BLOCK_DURATION_MS = 10_000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'rediscovery-')); +// Tests that nodes can rediscover each other from their stored peer tables after a full restart, +// without a bootstrap node. Uses P2PNetworkTest real libp2p, SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES +// (ethSlot=4s, aztecSlot=24s, proofSubEpochs=640), 4 validators, inboxLag=2. describe('e2e_p2p_rediscovery', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -53,6 +55,11 @@ describe('e2e_p2p_rediscovery', () => { } }); + // Forms an initial 4-node mesh, stops the bootstrap node, then restarts each validator from its data + // directory without any bootstrap ENR. Submits txs to each restarted node and asserts they mine, + // proving that discv5 peer-store entries are sufficient for re-discovery. + // REFACTOR: sequential sleep(2500) between node restarts is hand-rolled; the delay exists to avoid + // port conflicts but should be replaced with a port-readiness check or staggered createNode calls it('should re-discover stored peers without bootstrap node', async () => { const txsSentViaDifferentNodes: TxHash[][] = []; nodes = await createNodes( @@ -67,8 +74,9 @@ describe('e2e_p2p_rediscovery', () => { shouldCollectMetrics(), ); - // wait a bit for peers to discover each other - await sleep(8000); + // Wait for the nodes to form a full mesh before tearing down, so each one persists every peer's + // ENR to its store. Without this guarantee there would be nothing to rediscover after the restart. + await t.waitForP2PMeshConnectivity(nodes, NUM_VALIDATORS, 120); // We need to `createNodes` before we setup account, because // those nodes actually form the committee, and so we cannot build @@ -78,16 +86,17 @@ describe('e2e_p2p_rediscovery', () => { // stop bootstrap node await t.bootstrapNode?.stop(); - // create new nodes from datadir - const newNodes: AztecNodeService[] = []; - - // stop all nodes + // Bring the whole network down before bringing any node back up. This is the real test of + // rediscovery: with every node stopped there are no live peers left to dial the restarted nodes, + // so they can only rejoin by re-seeding discovery from the peers they persisted to disk. for (let i = 0; i < NUM_VALIDATORS; i++) { - const node = nodes[i]; - await node.stop(); + await nodes[i].stop(); t.logger.info(`Node ${i} stopped`); - await sleep(2500); + } + // recreate all nodes from their data dirs, without a bootstrap node + const newNodes: AztecNodeService[] = []; + for (let i = 0; i < NUM_VALIDATORS; i++) { const newNode = await createNode( t.ctx.aztecNodeConfig, t.ctx.dateProvider, diff --git a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts index 59527d18f17e..9ea60e96833a 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts @@ -27,6 +27,11 @@ const NUM_TXS_PER_NODE = 1; const BASE_BOOT_NODE_UDP_PORT = 4500; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'reex-')); +// Sets up a 4-node real libp2p network with txTimeoutMs=30s, proofSubEpochs=1024, min/max=1 txPerBlock, +// inboxLag=2 (v5 always enforces the timetable, so the former enforceTimeTable override is gone). The +// beforeAll submits transactions and deploys a spam contract +// in preparation for validator re-execution tests. All inner test logic is inside describe.skip +// pending a fix to the makeBlockBuilderDeps spy (TODO palla/mbps). describe('e2e_p2p_reex', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp.test.ts index 18b9e94818b5..010ec516f263 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp.test.ts @@ -11,6 +11,10 @@ const DATA_DIR = createReqrespDataDir(); // publish exceeds the default 5 min jest test timeout. Allow 15 min. jest.setTimeout(15 * 60 * 1000); +// Tests the reqresp tx-collection path over real libp2p: 6 validators, ethSlot=8s, aztecSlot=36s, +// blockDurationMs=6s, enforceTimeTable, min=1/max=2 txs, proofSubEpochs=1024, epoch=64 (stable committee), +// inboxLag=2. Non-proposer nodes have tx gossip disabled so they must request the tx over reqresp. +// Also verifies multi-blocks-per-slot (mbps) checkpoint is produced. jest.setTimeout=15m. describe('e2e_p2p_reqresp_tx', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -35,6 +39,8 @@ describe('e2e_p2p_reqresp_tx', () => { * * Note: we do not attempt to let this node produce a block, as it will not have received any transactions * from the other pxes. + * + * Delegates to runReqrespTxTest in utils.ts; see that helper for the full flow. */ nodes = await runReqrespTxTest({ t, dataDir: DATA_DIR }); }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp_no_handshake.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp_no_handshake.test.ts index 48d503d127d7..ed9337d96a49 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp_no_handshake.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp/reqresp_no_handshake.test.ts @@ -8,6 +8,8 @@ import { cleanupReqrespTest, createReqrespDataDir, createReqrespTest, runReqresp // TODO: DELETE THIS FILE // This is a temporary copy of reqresp.test.ts with status handshake disabled // Delete this file once we have settled on the cause of the reqresp flakes. +// Identical to reqresp.test.ts except p2pDisableStatusHandshake:true. Created to isolate flake root +// cause. Should be deleted once the investigation is complete. dup:reqresp/reqresp.test.ts const DATA_DIR = createReqrespDataDir(); @@ -15,6 +17,9 @@ const DATA_DIR = createReqrespDataDir(); // publish exceeds the default 5 min jest test timeout. Allow 15 min. jest.setTimeout(15 * 60 * 1000); +// Same setup as reqresp.test.ts (6 validators, real libp2p, ethSlot=8s, aztecSlot=36s, +// enforceTimeTable, proofSubEpochs=1024, epoch=64, inboxLag=2) but with p2pDisableStatusHandshake:true. +// Temporary copy pending flake investigation. See reqresp.test.ts for full description. describe('e2e_p2p_reqresp_tx_no_handshake', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -39,6 +44,8 @@ describe('e2e_p2p_reqresp_tx_no_handshake', () => { * * Note: we do not attempt to let this node produce a block, as it will not have received any transactions * from the other pxes. + * + * Identical to reqresp.test.ts but with status handshake disabled for flake investigation. */ nodes = await runReqrespTxTest({ t, dataDir: DATA_DIR, disableStatusHandshake: true }); }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/sentinel_status_slash.parallel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/sentinel_status_slash.parallel.test.ts index 63eace9b379d..31b69d2cdc07 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/sentinel_status_slash.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/sentinel_status_slash.parallel.test.ts @@ -22,7 +22,13 @@ import { awaitCommitteeExists, findUpcomingProposerSlot } from './shared.js'; /** * Exercises the sentinel's six-case proposer-status taxonomy end-to-end by driving each of the - * status variants via in-tree validator-config flags (no jest stubbing of internals): + * status variants via in-tree validator-config flags (no jest stubbing of internals). + * + * Setup: P2PNetworkTest with mockGossipSubNetwork:true (in-memory bus, NOT real libp2p). 6 validators, + * ethSlot varies by CI (4s local / 8s CI), aztecSlot=2x ethSlot, epoch=2, proofSubEpochs=1024, + * minTxsPerBlock=0, inboxLag=2, sentinelEnabled, fake prover (startProverNode:true). + * Each it runs as an isolated CI job (parallel convention). + * Candidate for relocation to e2e_slashing/. * * 1. `checkpoint-unvalidated` (case 3) — one validator runs with `broadcastInvalidBlockProposal`, * so honest observers reject its block proposals (state_mismatch) and never push them to @@ -136,6 +142,9 @@ describe('e2e_p2p_sentinel_status_slash', () => { } }); + // Spawns one malicious node with broadcastInvalidBlockProposal:true; honest observers reject via + // re-execution state_mismatch and record `checkpoint-unvalidated` for that proposer slot. The sentinel + // then emits an INACTIVITY offense. Asserts all honest observers agree on the fault slot and status. it('slashes the proposer with INACTIVITY when checkpoint validation records unvalidated', async () => { // One malicious node broadcasts invalid block proposals; honest observers reject them via // re-execution state_mismatch and therefore never push to their archivers, so the malicious @@ -158,6 +167,10 @@ describe('e2e_p2p_sentinel_status_slash', () => { await assertInactivityOffenseFor(targetAddress, nodes[1]); }); + // Spawns one malicious node with broadcastInvalidCheckpointProposalOnly:true; block proposals are + // valid (land in archivers) but checkpoint proposals carry a random archive. Observers detect + // header_mismatch and record `checkpoint-invalid`. The sentinel emits INACTIVITY. Asserts all + // observers agree and the malicious node self-records `checkpoint-valid`. it('slashes the proposer with INACTIVITY when checkpoint validation records invalid', async () => { // One malicious node broadcasts invalid CHECKPOINT proposals while keeping the underlying // block proposals valid; observers accept the blocks (so they land in the archiver) but @@ -173,6 +186,9 @@ describe('e2e_p2p_sentinel_status_slash', () => { await assertInactivityOffenseFor(targetAddress, nodes[1]); }); + // Starts 6 honest validators, waits for P2P mesh and committee, then stops the last validator. + // Asserts that all remaining observers record `attestation-missed` for the stopped node and that + // an INACTIVITY offense is emitted for it. it('slashes an attestor that gets stopped after the network is running', async () => { nodes = await createNodes( t.ctx.aztecNodeConfig, diff --git a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts index dc7fa791e0a4..95357b48f60a 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts @@ -57,6 +57,10 @@ const VETOER_ADDRESS = EthAddress.fromString( const SLASH_OFFSET_IN_ROUNDS = 2; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'slash-veto-demo-')); +// Tests the slasher veto mechanism. Uses P2PNetworkTest real libp2p: 3 running nodes + 1 +// registered-but-offline validator, fake prover. ethSlot=4s, aztecSlot=8s, epoch=2, +// proofSubEpochs=1024, minTxsPerBlock=0, inboxLag=2, sentinelEnabled, slashSelfAllowed, +// slashingVetoer=VETOER_ADDRESS (derived deterministically). Tests vetoPayload on the Slasher contract. describe('veto slash', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -172,6 +176,10 @@ describe('veto slash', () => { }); } + // Waits for the inactive validator to accumulate inactivity offenses reaching quorum, then the vetoer + // calls vetoPayload on the Slasher contract. Asserts the payload either expires (lifetime exceeded) + // or a later round is executed, and that the inactive validator's GSE balance is unchanged. + // Currently parameterised as shouldVeto=true only (the non-veto branch is present but not exercised). it.each([[true]] as const)( 'vetoes %s a slashing payload', async (shouldVeto: boolean) => { diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index bdbd2b57a985..29d76b10f0bf 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -34,6 +34,10 @@ jest.setTimeout(1000 * 60 * 10); /** * This tests emulate the same test as in l1-contracts/test/governance/scenario/UpgradeGovernanceProposerTest.t.sol * but it does so in an end-to-end manner with multiple "real" nodes. + * + * Setup: P2PNetworkTest real libp2p, SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES (ethSlot=4s, aztecSlot=12s, + * proofSubEpochs=640), 4 validators, governanceProposerRoundSize=10, activationThreshold=1e22, + * ejectionThreshold=5e21, minTxsPerBlock=0, inboxLag=2. No prover. jest.setTimeout=10m. */ describe('e2e_p2p_governance_proposer', () => { let t: P2PNetworkTest; @@ -73,6 +77,10 @@ describe('e2e_p2p_governance_proposer', () => { } }); + // Creates 4 validator nodes configured to signal a new GovernanceProposerPayload. Waits for quorum, + // warps past round boundary, submits the round winner, then drives the full governance lifecycle + // (vote, execution delay, execute). Asserts the governance contract's governanceProposer changes. + // REFACTOR: while(true) + sleep(12000) polling for quorum is hand-rolled; replace with retryUntil it('should cast votes to upgrade governanceProposer', async () => { // create the bootstrap node for the network if (!t.bootstrapNodeEnr) { diff --git a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts index 7bb7003a8ee0..4dd15eb240e6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts @@ -26,6 +26,10 @@ const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'validators-sentinel-')); jest.setTimeout(1000 * 60 * 10); +// Tests sentinel observability: 5 running validators + 1 registered-but-offline validator (6 total), +// fake prover. P2PNetworkTest real libp2p, ethSlot=4s, aztecSlot=8s, epoch=2, proofSubEpochs=1024, +// minTxsPerBlock=0, sentinelEnabled, slashInactivityPenalty=0 (slashing disabled). Also regression-tests +// that a late-joining node initialises its sentinel from the chain state (issue #13142). describe('e2e_p2p_validators_sentinel', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; @@ -85,6 +89,9 @@ describe('e2e_p2p_validators_sentinel', () => { } }); + // Suite that runs with one registered-but-offline validator. The beforeAll waits for the sentinel + // to accumulate history across BLOCK_COUNT checkpoints, then each it asserts different facets of + // the collected stats (offline validator, block builder, attestor). describe('with an offline validator', () => { let stats: ValidatorsStats; beforeAll(async () => { @@ -136,6 +143,8 @@ describe('e2e_p2p_validators_sentinel', () => { t.logger.info(`Collected validator stats at block ${t.monitor.checkpointNumber}`, { stats }); }); + // Asserts the offline validator's entire sentinel history consists only of missed entries and that + // missedAttestations.rate == 1. it('collects stats on offline validator', () => { t.logger.info(`Asserting stats for offline validator ${offlineValidator}`); const offlineStats = stats.stats[offlineValidator.toString().toLowerCase()]; @@ -147,6 +156,7 @@ describe('e2e_p2p_validators_sentinel', () => { expect(offlineStats.missedProposals.rate).toBeOneOf([1, NaN, undefined]); }); + // Finds a validator with a checkpoint-mined history entry and asserts its missedProposals.rate < 1. it('collects stats on a block builder', () => { const [proposerValidator, proposerStats] = Object.entries(stats.stats).find(([_, v]) => v?.history?.some(h => h.status === 'checkpoint-mined'), @@ -158,6 +168,7 @@ describe('e2e_p2p_validators_sentinel', () => { expect(proposerStats.missedProposals.rate).toBeLessThan(1); }); + // Finds a validator with an attestation-sent history entry and asserts its missedAttestations.rate < 1. it('collects stats on an attestor', () => { const [attestorValidator, attestorStats] = Object.entries(stats.stats).find(([_, v]) => v?.history?.some(h => h.status === 'attestation-sent'), @@ -169,7 +180,8 @@ describe('e2e_p2p_validators_sentinel', () => { expect(attestorStats.missedAttestations.rate).toBeLessThan(1); }); - // Regression test for #13142 + // Regression test for #13142: a fresh node that joins after several blocks should initialise its + // sentinel from chain state and accumulate history across subsequent slots. it('starts a sentinel on a fresh node', async () => { const checkpointNumber = t.monitor.checkpointNumber; const nodeIndex = NUM_NODES + 1; diff --git a/yarn-project/end-to-end/src/e2e_partial_notes.test.ts b/yarn-project/end-to-end/src/e2e_partial_notes.test.ts index ec1e4be32ac2..405d056a181e 100644 --- a/yarn-project/end-to-end/src/e2e_partial_notes.test.ts +++ b/yarn-project/end-to-end/src/e2e_partial_notes.test.ts @@ -11,6 +11,8 @@ import { setup } from './fixtures/utils.js'; const TIMEOUT = 300_000; +// Smoke test for the partial-note pattern: minting tokens into a private note via the +// Token contract's mint_to_private path. Single node with AutomineSequencer. describe('partial notes', () => { jest.setTimeout(TIMEOUT); @@ -41,6 +43,8 @@ describe('partial notes', () => { afterAll(() => teardown()); + // Calls mintTokensToPrivate to mint INITIAL_TOKEN_BALANCE tokens to the liquidity provider's + // private balance via the partial-note flow, then asserts the private balance equals the mint amount. it('mint to private', async () => { await mintTokensToPrivate(token0, adminAddress, liquidityProviderAddress, INITIAL_TOKEN_BALANCE); expect( diff --git a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts index 30d3e84d9c5b..0604823ed985 100644 --- a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts @@ -14,6 +14,9 @@ import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; import type { TestWallet } from './test-wallet/test_wallet.js'; +// Verifies the kernel's pending-note-hash squashing logic: notes created and nullified in the same tx +// are not persisted to the tree. Uses a single node with AutomineSequencer; contracts are deployed +// per-test via deployContract(). describe('e2e_pending_note_hashes_contract', () => { let aztecNode: AztecNode; let wallet: TestWallet; @@ -90,6 +93,8 @@ describe('e2e_pending_note_hashes_contract', () => { return contract; }; + // Inserts a note and reads it back within the same nested-call chain; asserts the tx succeeds, + // confirming the simulator can access pending (not-yet-persisted) notes within a single tx. it('Aztec.nr function can "get" notes it just "inserted"', async () => { const mintAmount = 65n; @@ -101,6 +106,8 @@ describe('e2e_pending_note_hashes_contract', () => { .send({ from: owner }); }); + // Creates one note and nullifies it in the same tx; asserts both the note hash and its nullifier + // are squashed (zeroed) in the mined block, and the private log count is zero. it('Squash! Aztec.nr function can "create" and "nullify" note in the same TX', async () => { // Kernel will squash the noteHash and its nullifier. // Realistic way to describe this test is "Mint note A, then burn note A in the same transaction" @@ -148,6 +155,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(1); }); + // Same as above but the insert emits two private logs; asserts all are squashed along with the note + // hash and nullifier. it('Squash! Aztec.nr function can "create" and "nullify" note in the same TX with 2 note logs', async () => { // Kernel will squash the noteHash and its nullifier and both note logs // Realistic way to describe this test is "Mint note A, then burn note A in the same transaction" @@ -171,6 +180,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(0); }); + // Creates two notes and nullifies both in the same tx; asserts both note hashes, both nullifiers, + // and both private logs are squashed. it('Squash! Aztec.nr function can "create" 2 notes and "nullify" both in the same TX', async () => { // Kernel will squash both noteHashes and their nullifier. // Realistic way to describe this test is "Mint notes A and B, then burn both in the same transaction" @@ -194,6 +205,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(0); }); + // Creates two notes but only nullifies one in the same tx; asserts exactly one note hash persists + // and its counterpart is squashed, leaving one private log. it('Squash! Aztec.nr function can "create" 2 notes and "nullify" 1 in the same TX (kernel will squash one note + nullifier)', async () => { // Kernel will squash one noteHash and its nullifier. // The other note will become persistent! @@ -218,6 +231,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(1); }); + // Same as the previous test but both notes share the same inner hash (static randomness); verifies + // that only one of the two identical-hash notes is squashed, and the other persists. it('Squash! Aztec.nr function can "create" 2 notes with the same note hash and "nullify" 1 in the same TX', async () => { // Kernel will squash one noteHash and its nullifier, where two notes with the same inner hash exist. // The other note will become persistent! @@ -242,6 +257,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(1); }); + // Creates a persistent note in tx1; in tx2 creates another note and nullifies both. Asserts the + // pending note's hash and its nullifier are squashed while the persistent note's nullifier persists. it('Squash! Aztec.nr function can nullify a pending note and a persistent in the same TX', async () => { // Create 1 note in isolated TX. // Then, in a separate TX, create 1 new note and nullify BOTH notes. @@ -281,6 +298,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(0); }); + // Creates a note in tx1; in tx2 nullifies it and calls get_note. Asserts the nullifier persists + // (the note was already in the tree) and get_note correctly returns nothing. it('get_notes function filters a nullified note created in a previous transaction', async () => { // Create a note in an isolated transaction. // In a subsequent transaction, we nullify the note and a call to 'get note' should @@ -311,6 +330,8 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNullifiersSquashedExcept(1); }); + // Emits more notes than MAX_NOTE_HASHES_PER_TX across nested calls by recycling (create+nullify each note), + // forcing the kernel reset circuit; asserts the tx succeeds without hitting the data structure limit. it('Should handle overflowing the kernel data structures in nested calls', async () => { // This test verifies that a transaction can emit more notes than MAX_NOTE_HASHES_PER_TX without failing, since // the notes are nullified and will be squashed by the kernel reset circuit. diff --git a/yarn-project/end-to-end/src/e2e_phase_check.test.ts b/yarn-project/end-to-end/src/e2e_phase_check.test.ts index bf72a88f06d3..5dcb027a5e1a 100644 --- a/yarn-project/end-to-end/src/e2e_phase_check.test.ts +++ b/yarn-project/end-to-end/src/e2e_phase_check.test.ts @@ -15,6 +15,9 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; // Private functions should receive automatically a phase check that avoids any nested call changing the phase. // Functions that opt out of this phase check can be marked with #[allow_phase_change]. +// +// Uses a single node with AutomineSequencer. The setup pre-funds a custom SponsoredFPC via genesisPublicData +// so that fee payment can be crafted without ending the setup phase, which is needed to trigger the phase-change error. describe('Phase check', () => { let wallet: TestWallet; let defaultAccountAddress: AztecAddress; @@ -53,6 +56,8 @@ describe('Phase check', () => { return new PublicDataTreeLeaf(balanceLeafSlot, defaultInitialAccountFeeJuice); } + // Simulates a tx that calls a function which internally invokes a nested call that ends the setup phase, + // triggering the automatic phase-check guard; asserts the simulation throws with the expected message. it('should fail when a nested call changes the phase', async () => { await expect( contract.methods.call_function_that_ends_setup().simulate({ @@ -64,6 +69,8 @@ describe('Phase check', () => { ).rejects.toThrow('Phase change detected on function with phase check.'); }); + // Same scenario but the function is annotated with #[allow_phase_change]; asserts the simulation + // succeeds without throwing. it('should not fail when a nested call changes the phase if #[allow_phase_change] is used', async () => { await contract.methods.call_function_that_ends_setup_without_phase_check().simulate({ from: defaultAccountAddress, diff --git a/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts b/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts index 98065f606d98..3dcbd0157746 100644 --- a/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_private_voting_contract.test.ts @@ -8,6 +8,8 @@ import { TX_ERROR_EXISTING_NULLIFIER } from '@aztec/stdlib/tx'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Verifies the PrivateVoting contract's nullifier-based double-vote prevention. Uses a single node +// with AutomineSequencer and one account. describe('e2e_voting_contract', () => { let wallet: Wallet; @@ -33,7 +35,10 @@ describe('e2e_voting_contract', () => { afterAll(() => teardown()); + // Suite covering the cast_vote flow including double-vote rejection via existing-nullifier error. describe('votes', () => { + // Starts a vote, casts once, then verifies the tally is 1. Attempts a second vote via simulate + // (expects nullifier collision) and then via send (expects TX_ERROR_EXISTING_NULLIFIER). it('votes, then tries to vote again', async () => { const candidate = new Fr(1); const electionId = { id: Fr.random() }; diff --git a/yarn-project/end-to-end/src/e2e_prover/client.test.ts b/yarn-project/end-to-end/src/e2e_prover/client.test.ts index f7821478e99a..f4e8aaa4001b 100644 --- a/yarn-project/end-to-end/src/e2e_prover/client.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/client.test.ts @@ -18,6 +18,11 @@ const TIMEOUT = 1_200_000; // prover-node startup) exceeds the default 5min jest per-test budget. jest.setTimeout(15 * 60 * 1000); +// Tests client-side proof generation and verification for private and public transfers. +// FullProverTest sets up a single node with a real prover node (real BB when FAKE_PROOFS=0, +// fake proofs otherwise) via PIPELINING_SETUP_OPTS (ethSlot=4s, aztecSlot=12s). The prover +// node is a second AztecNodeService with enableProverNode. No on-chain proof submission — only +// client-side circuit proof generation and circuitProofVerifier.verifyProof() are tested. describe('client_prover', () => { const REAL_PROOFS = !parseBooleanEnv(process.env.FAKE_PROOFS); const COINBASE_ADDRESS = EthAddress.random(); @@ -62,6 +67,9 @@ describe('client_prover', () => { await t.tokenSim.check(); }); + // Verifies fee juice portal has a balance, then proves a private transfer and a public transfer + // client-side (proveInteraction), and calls circuitProofVerifier.verifyProof() on each without + // submitting to the network. it( 'proves and verifies the client-side portion of private and public transfers', async () => { diff --git a/yarn-project/end-to-end/src/e2e_prover/full.test.ts b/yarn-project/end-to-end/src/e2e_prover/full.test.ts index 615a54b23725..42d6f0a09513 100644 --- a/yarn-project/end-to-end/src/e2e_prover/full.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/full.test.ts @@ -37,6 +37,11 @@ const TIMEOUT = REAL_PROOFS ? 45 * 60 * 1000 : 15 * 60 * 1000; // prover-node startup) doesn't time out. jest.setTimeout(TIMEOUT); +// End-to-end proof pipeline: client proves transactions, submits to node, sequencer builds blocks, +// prover node generates epoch proofs, and L1 verifies them. FullProverTest uses real BB proofs when +// FAKE_PROOFS=0 (CI_FULL only); fake proofs otherwise. Setup: PIPELINING_SETUP_OPTS (ethSlot=4s, +// aztecSlot=12s). Timeout is 45 min real / 15 min fake. Time-warp: cheatCodes.rollup.advanceToNextEpoch. +// jest.setTimeout(TIMEOUT) is 45 min for real proofs, 15 min for fake proofs. describe('full_prover', () => { const COINBASE_ADDRESS = EthAddress.random(); const t = new FullProverTest('full_prover', 1, COINBASE_ADDRESS, REAL_PROOFS); diff --git a/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts b/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts index c241727c9aeb..7918378715b5 100644 --- a/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts +++ b/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts @@ -14,6 +14,10 @@ import { setup } from './fixtures/utils.js'; // Tests PXE interacting with a node that has pruned relevant blocks, preventing usage of the archive API (which PXE // should not rely on). +// +// Uses a single node with AutomineSequencer, worldStateCheckpointHistory=2, and +// aztecProofSubmissionEpochs=1024 (effectively no reorg). markAsProven + extra L1 blocks cause world-state +// to prune old block data; the test then verifies that PXE can still discover notes from pruned blocks. describe('e2e_pruned_blocks', () => { jest.setTimeout(5 * 60 * 1000); @@ -68,6 +72,9 @@ describe('e2e_pruned_blocks', () => { } } + // Mints half the token amount (tx1), mines enough empty blocks to make that block eligible for pruning, + // calls markAsProven + extra L1 blocks to finalize the prune, polls until the archive query on tx1's + // block fails, then mints the other half and transfers the full amount. Asserts final balances. it('can discover and use notes created in both pruned and available blocks', async () => { // This is the only test in this suite so it doesn't seem worthwhile to worry too much about reusable setup etc. For // simplicity's sake I just did the entire thing here. @@ -106,6 +113,8 @@ describe('e2e_pruned_blocks', () => { // The same historical query we performed before should now fail since this block is not available anymore. We poll // the node for a bit until it processes the blocks we marked as proven, causing the historical query to fail. logger.warn(`Awaiting 'unable to find leaf' error from node due to pruned history`); + // REFACTOR: hand-rolled poll waiting for world-state prune to propagate; a DSL helper such as + // waitForWorldStatePrune(node, blockNumber) should replace this retryUntil loop. await retryUntil( async () => { try { diff --git a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts index f128bb6c8984..8aacc0691076 100644 --- a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts @@ -15,6 +15,9 @@ import { setup } from '../fixtures/utils.js'; // process.env.ETHEREUM_HOSTS= 'https://sepolia.infura.io/v3/'; // process.env.L1_CHAIN_ID = '11155111'; +// Public testnet transfer test. Calls setup() with PIPELINING_SETUP_OPTS but requires Sepolia credentials +// (SEQ_PUBLISHER_PRIVATE_KEY, ETHEREUM_HOSTS, L1_CHAIN_ID=11155111). CI-excluded; runs manually against a +// live public L1 network. Not a candidate for in-proc consolidation. describe(`deploys and transfers a private only token`, () => { let wallet: Wallet; diff --git a/yarn-project/end-to-end/src/e2e_publisher_funding_multi.test.ts b/yarn-project/end-to-end/src/e2e_publisher_funding_multi.test.ts index 940174e33e24..18f171816d89 100644 --- a/yarn-project/end-to-end/src/e2e_publisher_funding_multi.test.ts +++ b/yarn-project/end-to-end/src/e2e_publisher_funding_multi.test.ts @@ -41,6 +41,9 @@ const toPrivateKeyHex = (index: number): Hex => { const FUNDING_THRESHOLD = parseEther('2'); const FUNDING_AMOUNT = parseEther('2.1'); +// Tests the PublisherManager's automatic L1 ETH top-up logic when a keystore carries two publishers and a +// dedicated funding account. Uses PIPELINING_SETUP_OPTS (prod sequencer, ethSlot=4s, aztecSlot=12s) with +// a pre-written keystore JSON and ethCheatCodes to drain and monitor publisher balances. describe('e2e_publisher_funding_multi', () => { jest.setTimeout(5 * 60 * 1000); @@ -107,6 +110,9 @@ describe('e2e_publisher_funding_multi', () => { await rm(keyStoreDirectory, { recursive: true, force: true }); }); + // Sets both publisher L1 balances below the funding threshold via ethCheatCodes, waits for the + // PublisherManager's periodic funding loop to top them both up (round 1), then drains one publisher + // again and waits for a second funding round to confirm the loop is still healthy. it('funds both publishers when balances drop below threshold', async () => { const publishers: L1TxUtils[] = (publisherManager as any).publishers; const funder: L1TxUtils | undefined = (publisherManager as any).funder; @@ -130,6 +136,8 @@ describe('e2e_publisher_funding_multi', () => { // The RunningPromise checks funding every 2 minutes, so we need to wait long enough // for the next cycle to detect the low balances and fund both publishers. + // REFACTOR: hand-rolled poll waiting for PublisherManager funding cycle; a helper like + // waitForPublisherBalancesAbove(publisherManager, threshold) should replace this retryUntil. await retryUntil( async () => { const balance1 = await ethCheatCodes.getBalance(publisher1Address); @@ -166,6 +174,8 @@ describe('e2e_publisher_funding_multi', () => { const funderBalanceBefore2 = await ethCheatCodes.getBalance(funderAddress); logger.info(`Waiting for second funding round`); + // REFACTOR: hand-rolled poll waiting for a second PublisherManager funding cycle; same helper + // as above should cover this site. await retryUntil( async () => { const spent = funderBalanceBefore2 - (await ethCheatCodes.getBalance(funderAddress)); diff --git a/yarn-project/end-to-end/src/e2e_pxe.test.ts b/yarn-project/end-to-end/src/e2e_pxe.test.ts index 8b34d702cac0..ce0092793338 100644 --- a/yarn-project/end-to-end/src/e2e_pxe.test.ts +++ b/yarn-project/end-to-end/src/e2e_pxe.test.ts @@ -9,6 +9,8 @@ import type { TestWallet } from './test-wallet/test_wallet.js'; // TODO: Ideally these would be unit tests for PXE, but some functions like simulateTx, proveTx, etc require // more complex setup +// +// Exercises PXE simulation error paths that require a running node. Single node with AutomineSequencer. describe('e2e_pxe', () => { let wallet: TestWallet; let defaultAccountAddress: AztecAddress; @@ -27,6 +29,8 @@ describe('e2e_pxe', () => { afterAll(() => teardown()); + // Emits a nullifier on-chain, then simulates the same nullifier emission again; asserts the simulation + // throws an error that includes the TX_ERROR_EXISTING_NULLIFIER reason string. it('simulate includes validation reason in error', async () => { const nullifier = Fr.random(); await contract.methods.emit_nullifier(nullifier).send({ from: defaultAccountAddress }); diff --git a/yarn-project/end-to-end/src/e2e_scope_isolation.test.ts b/yarn-project/end-to-end/src/e2e_scope_isolation.test.ts index d12c4fe0d80f..3fe04430785a 100644 --- a/yarn-project/end-to-end/src/e2e_scope_isolation.test.ts +++ b/yarn-project/end-to-end/src/e2e_scope_isolation.test.ts @@ -5,6 +5,9 @@ import { ScopeTestContract } from '@aztec/noir-test-contracts.js/ScopeTest'; import { AUTOMINE_E2E_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Verifies that PXE note access and key-derivation are scoped per account: a different account +// cannot read another's notes or derive their nullifier hiding key. Uses a single node with +// AutomineSequencer and three accounts (alice, bob, charlie). describe('e2e scope isolation', () => { let wallet: Wallet; let accounts: AztecAddress[]; @@ -31,22 +34,27 @@ describe('e2e scope isolation', () => { afterAll(() => teardown()); + // Tests for external private functions: read_note (scoped to owner) and get_nhk (scoped to key holder). describe('external private', () => { + // Alice simulates read_note from her own scope; asserts the correct stored value is returned. it('owner can read own notes', async () => { const { result: value } = await contract.methods.read_note(alice).simulate({ from: alice }); expect(value).toEqual(ALICE_NOTE_VALUE); }); + // Bob attempts to read Alice's note from his scope; asserts simulation throws 'Failed to get a note'. it('cannot read notes belonging to a different account', async () => { await expect(contract.methods.read_note(alice).simulate({ from: bob })).rejects.toThrow('Failed to get a note'); }); + // Bob attempts to derive Charlie's nullifier hiding key; asserts 'Key validation request denied'. it('cannot access nullifier hiding key of a different account', async () => { await expect(contract.methods.get_nhk(charlie).simulate({ from: bob })).rejects.toThrow( 'Key validation request denied', ); }); + // Both Alice and Bob read their own notes on the shared wallet; asserts each sees only their value. it('each account can access their isolated state on a shared wallet', async () => { const { result: aliceValue } = await contract.methods.read_note(alice).simulate({ from: alice }); const { result: bobValue } = await contract.methods.read_note(bob).simulate({ from: bob }); @@ -56,24 +64,29 @@ describe('e2e scope isolation', () => { }); }); + // Same isolation checks repeated for external utility functions (read_note_utility, get_nhk_utility). describe('external utility', () => { + // Alice simulates read_note_utility from her own scope; asserts the correct stored value is returned. it('owner can read own notes', async () => { const { result: value } = await contract.methods.read_note_utility(alice).simulate({ from: alice }); expect(value).toEqual(ALICE_NOTE_VALUE); }); + // Bob attempts to read Alice's note via utility scope; asserts simulation throws 'Failed to get a note'. it('cannot read notes belonging to a different account', async () => { await expect(contract.methods.read_note_utility(alice).simulate({ from: bob })).rejects.toThrow( 'Failed to get a note', ); }); + // Bob attempts to derive Charlie's NHK via utility scope; asserts 'Key validation request denied'. it('cannot access nullifier hiding key of a different account', async () => { await expect(contract.methods.get_nhk_utility(charlie).simulate({ from: bob })).rejects.toThrow( 'Key validation request denied', ); }); + // Both Alice and Bob read via utility on the shared wallet; asserts each sees only their value. it('each account can access their isolated state on a shared wallet', async () => { const { result: aliceValue } = await contract.methods.read_note_utility(alice).simulate({ from: alice }); const { result: bobValue } = await contract.methods.read_note_utility(bob).simulate({ from: bob }); diff --git a/yarn-project/end-to-end/src/e2e_sequencer/escape_hatch_vote_only.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/escape_hatch_vote_only.test.ts index f08cdced0834..ca84715b3eff 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/escape_hatch_vote_only.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/escape_hatch_vote_only.test.ts @@ -37,6 +37,11 @@ const ESCAPE_HATCH_ACTIVE_DURATION = 16n; jest.setTimeout(1000 * 60 * 5); +// Tests the sequencer's behavior during an EscapeHatch voting window. One node running a 4-validator +// committee with pipelining opts (ethSlot=12s, aztecSlot=36s, epoch=4, proofSubEpochs=15). The +// beforeEach deploys a custom EscapeHatch L1 contract and wires it into the rollup. Timing driven by +// cheatCodes.rollup.advanceToEpoch + retryUntil waits. +// Setup: plain setup(1, { ...PIPELINING_SETUP_OPTS, overridden slots, aztecTargetCommitteeSize=4 }). describe('e2e_escape_hatch_vote_only', () => { let logger: Logger; let teardown: () => Promise; @@ -146,6 +151,9 @@ describe('e2e_escape_hatch_vote_only', () => { afterEach(() => teardown()); + // Verifies that when the escape hatch is open the sequencer casts governance votes every slot without + // building blocks or checkpoints, and that no failure events are emitted in the vote-only window. + // Waits two full epochs via retryUntil, then checks vote count >= slots elapsed and checkpoint count = 0. it('casts governance signals and advances checkpoints while escape hatch is closed', async () => { const sequencer = sequencerClient!.getSequencer(); @@ -229,6 +237,7 @@ describe('e2e_escape_hatch_vote_only', () => { const initialStats = await getStats(); // We will wait until epochs advance + // REFACTOR: retryUntil on epoch arithmetic should be replaced with a cheatCodes.rollup.waitForEpoch helper await retryUntil( async () => (await rollup.getEpochNumberForSlotNumber(await rollup.getSlotNumber())) >= initialStats.epoch + EpochNumber(2), @@ -245,6 +254,7 @@ describe('e2e_escape_hatch_vote_only', () => { const slotsPassed = slotAtMeasurement - initialStats.slot; expect(slotsPassed).toBeGreaterThan(0); const drainTarget = slotAtMeasurement + 2; + // REFACTOR: retryUntil on slot polling should be replaced with a rollup slot-wait helper await retryUntil( () => rollup.getSlotNumber().then(s => s >= drainTarget), 'pipelined vote drain', diff --git a/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.parallel.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.parallel.test.ts index ba0b9048a3c8..0391c0985deb 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/gov_proposal.parallel.test.ts @@ -41,6 +41,11 @@ const COMMITTEE_SIZE = 16; jest.setTimeout(1000 * 60 * 5); +// Tests that a single sequencer node running a 16-validator committee can propose blocks while +// simultaneously casting governance votes, and can cast votes even when block building is disabled. +// Setup: plain setup(1, { ...PIPELINING_SETUP_OPTS, ethSlot=8s, aztecSlot=16s, committee=16, +// proofSubEpochs=128 }) (v5 always enforces the timetable, so the former enforceTimeTable override is +// gone). Uses cheatCodes.eth.warp + retryUntil for timing. describe('e2e_gov_proposal', () => { let logger: Logger; let teardown: () => Promise; @@ -170,6 +175,9 @@ describe('e2e_gov_proposal', () => { expect(signals).toBeGreaterThanOrEqual(expectedMinVotes); }; + // Verifies that the sequencer proposes one block per slot (minTxsPerBlock=1) throughout an entire + // voting round while simultaneously casting a governance vote per slot. Sends roundDuration txs + // sequentially and waits for each to mine, then checks vote signals >= roundDuration. it('should propose blocks while voting', async () => { await aztecNodeAdmin!.setConfig({ governanceProposerPayload: newGovernanceProposerAddress, @@ -207,6 +215,9 @@ describe('e2e_gov_proposal', () => { await verifyVotes(round, roundDuration); }); + // Verifies that the sequencer still casts governance votes even when block sync is disabled + // (blob client disabled + skipPushProposedBlocksToArchiver). Disables sync shortcuts, confirms + // a tx times out as unminable, then waits a full round and asserts votes >= roundDuration. it('should vote even when unable to build blocks', async () => { const monitor = new ChainMonitor(rollup, dateProvider).start(); @@ -238,6 +249,7 @@ describe('e2e_gov_proposal', () => { // Check that the checkpoint number has indeed increased on L1 so sequencers cant pass the sync check. // Allow another slot for any in-flight L1 propose to mine, since the work loop above hits its wait timeout the // moment the tx misses L2 sync, not the moment the L1 tx lands. + // REFACTOR: retryUntil polling ChainMonitor should be replaced with a ChainMonitor.waitForCheckpoint helper const checkpointAfterBlobDisable = await retryUntil( async () => { const snapshot = await monitor.run(); @@ -275,6 +287,7 @@ describe('e2e_gov_proposal', () => { const nextRoundEndsAtSlot = SlotNumber(nextRoundBeginsAtSlot + Number(roundDuration)); const timeout = AZTEC_SLOT_DURATION * Number(roundDuration + 2n) + 20; logger.warn(`Waiting until slot ${nextRoundEndsAtSlot} for round to end (timeout ${timeout}s)`); + // REFACTOR: retryUntil on slot polling should be replaced with a rollup slot-wait helper await retryUntil(() => rollup.getSlotNumber().then(s => s > nextRoundEndsAtSlot), 'round end', timeout, 1); // We should have voted despite being unable to build blocks 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 601785e53a36..49eb682a0bb5 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 @@ -33,6 +33,9 @@ const VALIDATOR_COUNT = 4; const COMMITTEE_SIZE = VALIDATOR_COUNT; const INITIAL_KEYSTORE_COUNT = 3; +// REFACTOR: hand-rolled state-changed on/off subscription with a manual timeout that runs an action and +// waits for the sequencer to return to IDLE — a waitForSequencerState(IDLE, { after }) DSL helper should +// replace it (also duplicated as waitForSequencerIdle in e2e_multiple_blobs / e2e_fees / l2_to_l1). async function waitForSequencerIdleAfter( sequencer: TestSequencer, action: () => Promise, @@ -84,6 +87,10 @@ async function waitForSequencerIdleAfter( } } +// Tests that the sequencer node's keystore can be hot-reloaded at runtime via the admin API. +// One node with 4 validators staked, committee size 4. Initially only 3 validators are in the +// keystore; after reload all 4 are active with new coinbases. Uses PIPELINING_SETUP_OPTS +// (ethSlot=4s, aztecSlot=12s) with minTxsPerBlock=1, maxTxsPerBlock=1. describe('e2e_reload_keystore', () => { jest.setTimeout(540_000); @@ -165,6 +172,9 @@ describe('e2e_reload_keystore', () => { await rm(keyStoreDirectory, { recursive: true, force: true }); }); + // Verifies that after writing an updated keystore file and calling reloadKeystore(), the validator + // client exposes all 4 validators with new coinbases, and blocks subsequently mined carry one of + // the new per-validator coinbases rather than the old shared one. it('should reload keystore, add a new validator, and use updated coinbase in blocks', async () => { // Access the sequencer's validator client to inspect keystore state const sequencer = (sequencerClient! as TestSequencerClient).getSequencer() as TestSequencer; diff --git a/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts b/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts index da24120ff255..d1675055aaec 100644 --- a/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts +++ b/yarn-project/end-to-end/src/e2e_sequencer/slasher_config.test.ts @@ -5,6 +5,9 @@ import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client' import { PIPELINING_SETUP_OPTS } from '../fixtures/fixtures.js'; import { type EndToEndContext, setup } from '../fixtures/utils.js'; +// Tests that slasher configuration can be updated at runtime via the node admin API. +// Single node with no accounts (setup(0)), PIPELINING_SETUP_OPTS (ethSlot=4s, aztecSlot=12s), +// slasher enabled with custom inactivity config. No block building exercised. describe('e2e_slasher_config', () => { let aztecNodeAdmin: AztecNodeAdmin | undefined; let aztecNode: AztecNode; @@ -25,6 +28,9 @@ describe('e2e_slasher_config', () => { afterAll(() => teardown()); + // Reads the initial slasher config from the running node's slasher client, calls setConfig() via + // the admin API to update slashInactivityTargetPercentage, and asserts the new value is reflected + // while slashInactivityPenalty remains unchanged. it('should update slasher config', async () => { const slasherClient = (aztecNode as TestAztecNodeService).slasherClient as SlasherClientInterface; expect(slasherClient).toBeDefined(); 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 18eb85520648..e9ea8a84a858 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 @@ -15,6 +15,9 @@ import 'jest-extended'; import { PIPELINED_FEE_PADDING, PIPELINING_SETUP_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Verifies sequencer runtime configuration (maxL2BlockGas / manaTarget) via a live Bot. Uses +// PIPELINING_SETUP_OPTS (prod sequencer, ethSlot=4s, aztecSlot=12s) with no accounts pre-deployed; +// the bot creates its own account inline. describe('e2e_sequencer_config', () => { jest.setTimeout(20 * 60 * 1000); // 20 minutes @@ -30,6 +33,7 @@ describe('e2e_sequencer_config', () => { jest.restoreAllMocks(); }); + // Suite exercising sequencer.updateConfig() at runtime to assert mana/gas limits are respected. describe('Sequencer config', () => { // Sane targets < 64 bits. const manaTarget = 200e6; @@ -57,6 +61,7 @@ describe('e2e_sequencer_config', () => { afterAll(() => teardown()); + // Asserts that the sequencer client's maxL2BlockGas property reflects the value passed to setup(). it('properly sets config', () => { if (!sequencer) { throw new Error('Sequencer not found'); @@ -64,6 +69,8 @@ describe('e2e_sequencer_config', () => { expect(sequencer.maxL2BlockGas).toBe(manaTarget * 2); }); + // Runs a bot tx to measure actual mana used, then sets maxL2BlockGas to exactly that value + // (success expected), then to that value minus one (Timeout awaiting isMined expected). it('respects maxL2BlockGas', async () => { sequencer!.updateConfig({ maxTxsPerBlock: 1, 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 c58e75d68ef9..cd17bfed96e8 100644 --- a/yarn-project/end-to-end/src/e2e_simple.test.ts +++ b/yarn-project/end-to-end/src/e2e_simple.test.ts @@ -14,6 +14,9 @@ import 'jest-extended'; import { PIPELINING_SETUP_OPTS } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Basic smoke tests for the production sequencer + prover node path. Uses PIPELINING_SETUP_OPTS +// (prod sequencer, ethSlot=4s, aztecSlot=12s, epochDuration=4, fake prover via startProverNode). +// Exercises block-data queries and waits for a deployed tx to reach proven status. describe('e2e_simple', () => { jest.setTimeout(20 * 60 * 1000); // 20 minutes @@ -27,6 +30,7 @@ describe('e2e_simple', () => { jest.restoreAllMocks(); }); + // Suite exercising node block-data API and end-to-end deploy+prove flow with a fake prover. describe('A simple test', () => { const artifact = StatefulTestContractArtifact; @@ -52,6 +56,7 @@ describe('e2e_simple', () => { afterAll(() => teardown()); + // Fetches block 0 by number and by hash; asserts the returned blocks match and contain no txEffects. it('returns initial block data', async () => { const initialHeader = (await aztecNode.getBlockData(BlockNumber.ZERO))?.header; expect(initialHeader).toBeDefined(); @@ -66,6 +71,8 @@ describe('e2e_simple', () => { expect(initialBlockByNumber!.body.txEffects.length).toBe(0); }); + // Deploys StatefulTestContract via ContractDeployer and waits for the tx to reach proven status + // using waitForProven, exercising the full sequencer→prover-node→L1 proof submission path. it('deploys a contract', async () => { const deployer = new ContractDeployer(artifact, wallet); diff --git a/yarn-project/end-to-end/src/e2e_slashing/attested_invalid_proposal.test.ts b/yarn-project/end-to-end/src/e2e_slashing/attested_invalid_proposal.test.ts index 3b141fa5044c..6f222816300a 100644 --- a/yarn-project/end-to-end/src/e2e_slashing/attested_invalid_proposal.test.ts +++ b/yarn-project/end-to-end/src/e2e_slashing/attested_invalid_proposal.test.ts @@ -156,6 +156,11 @@ async function advanceToEpochBeforePipelinedTargetSlot({ throw new Error(`Target proposer ${targetProposer.toString()} not found after ${maxAttempts} epoch attempts`); } +// Tests slashing of a validator that attests to an invalid checkpoint proposal. Uses P2PNetworkTest +// with mockGossipSubNetwork: true (in-memory gossip bus, no real libp2p). Three validator nodes are +// created via createNode from setup_p2p_test.ts. Timing: ethSlot=4s, aztecSlot=36s, epoch=2, +// committee=3. RollupCheatCodes.advanceToEpoch drives progress; retryUntil waits for attestations +// and offenses. describe('e2e_slashing_attested_invalid_proposal', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[] = []; @@ -293,6 +298,7 @@ describe('e2e_slashing_attested_invalid_proposal', () => { targetSlot, }); + // REFACTOR: retryUntil polling pendingTxCount should be replaced with a waitForPendingTxCount helper await retryUntil( async () => { const pendingTxCount = await badProposerNode.getPendingTxCount(); @@ -456,6 +462,9 @@ describe('e2e_slashing_attested_invalid_proposal', () => { }; } + // Runs createInvalidProposalSlashingScenario with broadcastInvalidCheckpointProposalOnly=true so + // the bad proposer broadcasts a bad checkpoint proposal but no bad block proposal. Asserts the lazy + // attester receives an ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL offense; bad proposer is not slashed. it('slashes a lazy attester for an invalid checkpoint proposal', async () => { await createInvalidProposalSlashingScenario({ badProposerConfig: { @@ -466,6 +475,10 @@ describe('e2e_slashing_attested_invalid_proposal', () => { }); }); + // Runs createInvalidProposalSlashingScenario with broadcastEquivocatedProposals=true so the bad + // proposer equivocates. Asserts lazy attester is initially slashed; then broadcasts a delayed + // equivocated proposal and verifies the attestation offense is cleared and a DUPLICATE_PROPOSAL + // offense replaces it on the honest node. it('slashes a lazy attester for an invalid checkpoint and clears it on delayed equivocation', async () => { const { rollup, diff --git a/yarn-project/end-to-end/src/e2e_slashing/broadcasted_invalid_checkpoint_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_slashing/broadcasted_invalid_checkpoint_proposal_slash.test.ts index c6e9800bf187..e3a457c790e8 100644 --- a/yarn-project/end-to-end/src/e2e_slashing/broadcasted_invalid_checkpoint_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_slashing/broadcasted_invalid_checkpoint_proposal_slash.test.ts @@ -225,6 +225,11 @@ async function makeInvalidCheckpointProposals({ return { earlierBlock, terminalBlock, higherBlock, checkpoint }; } +// Tests slashing of a validator that broadcasts an invalid checkpoint proposal. Uses P2PNetworkTest +// with mockGossipSubNetwork: true (in-memory gossip bus, no real libp2p). Nodes created via +// createNode/createNodes from setup_p2p_test.ts. Timing: ethSlot=4s, aztecSlot=8s, epoch=2, +// committee=2. Three it() cases cover: checkpoint truncated below own block, mismatched header +// (live sequencer path), and valid checkpoint with delayed higher-index block. describe('e2e_slashing_broadcasted_invalid_checkpoint_proposal_slash', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[] = []; @@ -323,6 +328,9 @@ describe('e2e_slashing_broadcasted_invalid_checkpoint_proposal_slash', () => { return { node, currentSlot, signer, validator, signatureContext }; }; + // Manually broadcasts three block proposals for a past slot, then broadcasts a checkpoint that + // references only the earlier block (truncated below the terminal block). Asserts that + // awaitBroadcastedInvalidCheckpointOffense detects a BROADCASTED_INVALID_CHECKPOINT_PROPOSAL offense. it('slashes a validator that broadcasts a checkpoint truncated below its own retained block proposal', async () => { const { node, currentSlot, signer, validator, signatureContext } = await setupNodeAndValidator(); const targetSlot = SlotNumber(Number(currentSlot) - 2); @@ -360,6 +368,9 @@ describe('e2e_slashing_broadcasted_invalid_checkpoint_proposal_slash', () => { expect(firstOffense.amount).toEqual(slashingUnit); }); + // Runs a full sequencer cycle using broadcastInvalidCheckpointProposalOnly nodes; the invalid + // proposer broadcasts a checkpoint whose header does not match its own block. Honest nodes observe + // the offense via awaitAnyBroadcastedInvalidCheckpointOffense. it('slashes a validator that broadcasts a checkpoint with a mismatched header', async () => { const { rollup } = await t.getContracts(); @@ -433,6 +444,9 @@ describe('e2e_slashing_broadcasted_invalid_checkpoint_proposal_slash', () => { } }); + // Broadcasts a checkpoint that includes the terminal block in its lastBlock field (valid at first). + // Asserts no offense is recorded. Then broadcasts a higher-index block proposal, making the + // checkpoint invalid retroactively, and asserts the offense is now detected. it('does not slash a valid checkpoint whose lastBlock supplies the terminal proposal until a delayed higher-index block is retained', async () => { const { node, currentSlot, signer, validator, signatureContext } = await setupNodeAndValidator(); const targetSlot = SlotNumber(Number(currentSlot) - 2); diff --git a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts index 0ae9e0969f96..fae742cb1e99 100644 --- a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts +++ b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts @@ -22,6 +22,10 @@ const L1_BLOCK_TIME_IN_S = process.env.L1_BLOCK_TIME ? parseInt(process.env.L1_B const L2_TARGET_BLOCK_NUM = 3; const TARGET_CHECKPOINT_NUMBER = CheckpointNumber(3); +// Verifies the node snapshot upload/download sync path. Uses PIPELINING_SETUP_OPTS (prod sequencer, +// ethSlot=8s default or L1_BLOCK_TIME override, aztecSlot=2×ethSlot, epochDuration=64, no prover node). +// The suite runs sequentially: it1 waits for checkpoints, it2 creates a snapshot, it3/it4 sync new nodes +// from one or multiple snapshot URLs, including fallback from a corrupted snapshot. describe('e2e_snapshot_sync', () => { let context: EndToEndContext; let monitor: ChainMonitor; @@ -78,19 +82,30 @@ describe('e2e_snapshot_sync', () => { expect(worldState.latestBlockNumber).toBeGreaterThanOrEqual(blockNumber); }; + // Polls ChainMonitor until checkpointNumber exceeds TARGET_CHECKPOINT_NUMBER (3), establishing + // enough chain history for the subsequent snapshot tests. it('waits until a few checkpoints have been mined', async () => { log.warn(`Waiting for checkpoints to be mined`); + // REFACTOR: hand-rolled poll on ChainMonitor.checkpointNumber; EpochsTestContext.waitUntilCheckpointNumber + // or a shared helper should replace this retryUntil. await retryUntil(() => monitor.checkpointNumber > TARGET_CHECKPOINT_NUMBER, 'checkpoints-mined', 90, 1); log.warn(`Checkpoint height is now ${monitor.checkpointNumber}.`); }); + // Triggers a snapshot upload via aztecNodeAdmin.startSnapshotUpload(), then polls until at least + // one file appears in the snapshot directory. it('creates a snapshot', async () => { log.warn(`Creating snapshot`); await context.aztecNodeAdmin.startSnapshotUpload(snapshotLocation); + // REFACTOR: hand-rolled poll waiting for snapshot files to appear; a helper like + // waitForSnapshotUpload(adminNode, snapshotDir) should replace this. await retryUntil(() => readdir(snapshotDir).then(files => files.length > 0), 'snapshot-created', 90, 1); log.warn(`Snapshot created`); }); + // Starts a new non-validator node with syncMode='snapshot' pointing at the local snapshot URL; asserts + // the node syncs to at least L2_TARGET_BLOCK_NUM and that both the original and new node see the same + // block hash leaf in the archive tree. it('downloads snapshot when syncing new node', async () => { log.warn(`Syncing brand new node with snapshot sync`); const node = await createNonValidatorNode('1', { snapshotsUrls: [snapshotLocation], syncMode: 'snapshot' }); @@ -114,6 +129,9 @@ describe('e2e_snapshot_sync', () => { await node.stop(); }); + // Creates three snapshot locations: highest L1 block but corrupted (snapshot1), lowest L1 block (snapshot2), + // and the original valid middle-height snapshot (snapshot3). Syncs a new node with all three URLs and + // asserts it falls back past the corrupt snapshot to the next-best valid one (snapshot3). it('downloads snapshot from multiple sources', async () => { log.warn(`Setting up multiple snapshot locations with different L1 block heights`); diff --git a/yarn-project/end-to-end/src/e2e_state_vars.test.ts b/yarn-project/end-to-end/src/e2e_state_vars.test.ts index b94cfa307c13..91b22307ca83 100644 --- a/yarn-project/end-to-end/src/e2e_state_vars.test.ts +++ b/yarn-project/end-to-end/src/e2e_state_vars.test.ts @@ -14,6 +14,10 @@ import { proveInteraction } from './test-wallet/utils.js'; const TIMEOUT = 300_000; +// Exercises PublicImmutable, PrivateMutable, PrivateImmutable, and DelayedPublicMutable state variable types +// via the StateVars and Auth contracts. Single node with AutomineSequencer. The DelayedPublicMutable sub-suite +// reads DefaultL1ContractsConfig.aztecSlotDuration as a static constant (72) for delay arithmetic and asserts +// it has not changed; the runtime slot set by AUTOMINE_E2E_OPTS (12s) is irrelevant to that assertion. describe('e2e_state_vars', () => { jest.setTimeout(TIMEOUT); @@ -39,13 +43,19 @@ describe('e2e_state_vars', () => { afterAll(() => teardown()); + // Tests for PublicImmutable: initialize-once semantics, reading from private/public/utility contexts, + // and rejection of double-initialization. describe('PublicImmutable', () => { + // Simulates a constrained private read on an uninitialized PublicImmutable; asserts the expected + // error is thrown. it('private read of uninitialized PublicImmutable should fail', async () => { await expect( contract.methods.get_public_immutable_constrained_private().simulate({ from: defaultAccountAddress }), ).rejects.toThrow('Trying to read from uninitialized PublicImmutable'); }); + // Sends initialize_public_immutable(1) then reads back via get_public_immutable; asserts the + // returned struct's account field matches the caller. it('initialize and read PublicImmutable', async () => { // Initializes the public immutable and then reads the value using a utility function // checking the return values: @@ -57,6 +67,8 @@ describe('e2e_state_vars', () => { expect(read).toEqual({ account: defaultAccountAddress, value: read.value }); }); + // Reads the initialized PublicImmutable from two private contexts (direct and indirect) plus utility, + // via a BatchCall; asserts the direct read equals utility and the indirect read equals utility.value + 1. it('private read of initialized PublicImmutable', async () => { // Reads the value using a utility function checking the return values with: // 1. A constrained private function that reads it directly @@ -76,6 +88,8 @@ describe('e2e_state_vars', () => { await contract.methods.match_public_immutable(c.account, c.value).send({ from: defaultAccountAddress }); }); + // Same as the private-read test but using constrained public functions; asserts direct and indirect + // public reads are consistent with the utility value. it('public read of PublicImmutable', async () => { // Reads the value using a utility function checking the return values with: // 1. A constrained public function that reads it directly @@ -96,6 +110,8 @@ describe('e2e_state_vars', () => { await contract.methods.match_public_immutable(c.account, c.value).send({ from: defaultAccountAddress }); }); + // Calls get_public_immutable_constrained_public_multiple (reads 5 times in one function); asserts + // the returned array equals [c, c, c, c, c] where c is the utility read result. it('public multiread of PublicImmutable', async () => { // Reads the value using a utility function checking the return values with: // 1. A constrained public function that reads 5 times directly (going beyond the previous 4 Field return value) @@ -108,6 +124,8 @@ describe('e2e_state_vars', () => { expect(a).toEqual([c, c, c, c, c]); }); + // Calls initialize_public_immutable a second time after it was already initialized in the previous + // test (depends on sequential execution); asserts 'Attempted to emit duplicate nullifier'. it('initializing PublicImmutable the second time should fail', async () => { // Jest executes the tests sequentially and the first call to initialize_public_immutable was executed // in the previous test, so the call below should fail. @@ -117,7 +135,10 @@ describe('e2e_state_vars', () => { }); }); + // Tests for PrivateMutable: initialize, read, update, and rejection of re-initialization. describe('PrivateMutable', () => { + // Asserts is_private_mutable_initialized returns false before initialization, then confirms + // get_private_mutable throws on an uninitialized slot. it('fail to read uninitialized PrivateMutable', async () => { expect( ( @@ -131,6 +152,8 @@ describe('e2e_state_vars', () => { ).rejects.toThrow(); }); + // Sends initialize_private(RANDOMNESS, VALUE), verifies the tx produces 2 nullifiers (one for the + // tx and one for the initializer), and asserts is_private_mutable_initialized returns true after. it('initialize PrivateMutable', async () => { expect( ( @@ -157,6 +180,8 @@ describe('e2e_state_vars', () => { ).toEqual(true); }); + // Attempts to call initialize_private a second time; asserts it throws and the initialized flag + // remains true. it('fail to reinitialize', async () => { expect( ( @@ -177,6 +202,7 @@ describe('e2e_state_vars', () => { ).toEqual(true); }); + // Reads the PrivateMutable after initialization; asserts the stored value matches VALUE. it('read initialized PrivateMutable', async () => { expect( ( @@ -191,6 +217,8 @@ describe('e2e_state_vars', () => { expect(value).toEqual(VALUE); }); + // Calls update_private_mutable with the same RANDOMNESS and VALUE; asserts one new note hash and + // 2 nullifiers (tx + old note), and the stored value is unchanged. it('replace with same value', async () => { expect( ( @@ -219,6 +247,8 @@ describe('e2e_state_vars', () => { expect(noteBefore.value).toEqual(noteAfter.value); }); + // Calls update_private_mutable with different RANDOMNESS and VALUE; asserts one new note hash, + // 2 nullifiers, and the stored value matches the new VALUE. it('replace PrivateMutable with other values', async () => { expect( ( @@ -243,6 +273,8 @@ describe('e2e_state_vars', () => { expect(value).toEqual(VALUE + 1n); }); + // Calls increase_private_value (reads then updates in private); asserts the new value is exactly + // the prior value + 1, verifying read-then-write consistency. it('replace PrivateMutable dependent on prior value', async () => { expect( ( @@ -271,7 +303,10 @@ describe('e2e_state_vars', () => { }); }); + // Tests for PrivateImmutable: initialize-once semantics, reading the stored value, and rejection of + // double-initialization. describe('PrivateImmutable', () => { + // Asserts is_priv_imm_initialized is false before initialization and that view_private_immutable throws. it('fail to read uninitialized PrivateImmutable', async () => { expect( ( @@ -285,6 +320,8 @@ describe('e2e_state_vars', () => { ).rejects.toThrow(); }); + // Calls initialize_private_immutable(RANDOMNESS, VALUE); asserts 1 note hash and 2 nullifiers are + // emitted, and is_priv_imm_initialized becomes true. it('initialize PrivateImmutable', async () => { expect( ( @@ -311,6 +348,7 @@ describe('e2e_state_vars', () => { ).toEqual(true); }); + // Calls initialize_private_immutable a second time; asserts it throws and the flag remains true. it('fail to reinitialize', async () => { expect( ( @@ -331,6 +369,7 @@ describe('e2e_state_vars', () => { ).toEqual(true); }); + // Reads the PrivateImmutable after initialization; asserts the stored value matches VALUE. it('read initialized PrivateImmutable', async () => { expect( ( @@ -348,6 +387,8 @@ describe('e2e_state_vars', () => { }); }); + // Tests for DelayedPublicMutable: verifies that changing the authorized-delay alters the + // expirationTimestamp returned in private reads by the expected amount. describe('DelayedPublicMutable', () => { let authContract: AuthContract; @@ -366,6 +407,9 @@ describe('e2e_state_vars', () => { } }); + // Changes the authorized delay from 5 slots (360s) to 2 slots, advances the chain past the + // scheduled timestamp_of_change by sending no-op txs, then proves the private read and asserts + // the expirationTimestamp equals anchorTimestamp + newDelay - 1. it('sets the expiration timestamp property', async () => { // Mirrors CHANGE_AUTHORIZED_DELAY in noir-contracts/contracts/app/auth_contract/src/main.nr. const oldDelay = 360n; @@ -393,6 +437,8 @@ describe('e2e_state_vars', () => { // forces aztecSlotDuration=12s under pipelining (see fixtures/setup.ts), so a fixed // `delay(N blocks)` cannot count for the schedule — block timestamp polling is the // slot-duration-agnostic way to know we have crossed the schedule. + // REFACTOR: hand-rolled loop advancing the chain by sending no-op txs until a target timestamp is + // crossed; a DSL helper like advanceChainToTimestamp(node, timestampOfChange) should replace this. while ((await aztecNode.getBlockData('latest'))!.header.globalVariables.timestamp < timestampOfChange) { await authContract.methods.get_authorized().send({ from: defaultAccountAddress }); } diff --git a/yarn-project/end-to-end/src/e2e_static_calls.test.ts b/yarn-project/end-to-end/src/e2e_static_calls.test.ts index 5039644c6373..ea1d6087c4de 100644 --- a/yarn-project/end-to-end/src/e2e_static_calls.test.ts +++ b/yarn-project/end-to-end/src/e2e_static_calls.test.ts @@ -10,6 +10,9 @@ import { } from './fixtures/fixtures.js'; import { setup } from './fixtures/utils.js'; +// Verifies that static call enforcement prevents state modifications in private and public contexts, +// and that non-static calls to functions marked with static-call assertions are rejected. Uses a single +// node with AutomineSequencer and two contracts (StaticParent, StaticChild). describe('e2e_static_calls', () => { let wallet: Wallet; let parentContract: StaticParentContract; @@ -34,21 +37,26 @@ describe('e2e_static_calls', () => { afterAll(() => teardown()); + // Tests calling StaticChild methods directly: legal reads succeed, illegal state-modifying calls fail. describe('direct view calls to child', () => { + // Calls a read-only private function via a direct send; asserts the tx is mined without error. it('performs legal private static calls', async () => { await childContract.methods.private_get_value(42n, owner).send({ from: owner }); }); + // Calls a private function that illegally sets state; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing non-static calls to poorly written static private functions', async () => { await expect(childContract.methods.private_illegal_set_value(42n, owner).send({ from: owner })).rejects.toThrow( STATIC_CALL_STATE_MODIFICATION_ERROR, ); }); + // Calls a read-only public function; asserts it is mined without error. it('performs legal public static calls', async () => { await childContract.methods.pub_get_value(42n).send({ from: owner }); }); + // Simulates a public function that illegally increments state; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing non-static calls to poorly written static public functions', async () => { await expect(childContract.methods.pub_illegal_inc_value(42n).simulate({ from: owner })).rejects.toThrow( STATIC_CALL_STATE_MODIFICATION_ERROR, @@ -56,7 +64,10 @@ describe('e2e_static_calls', () => { }); }); + // Tests StaticParent routing calls to StaticChild: legal static calls succeed, illegal calls (state + // modification or assertion violations) throw the expected error strings. describe('parent calls child', () => { + // Parent calls child's private read-only function via low-level call and via typed interface; both succeed. it('performs legal private to private static calls', async () => { // Using low level calls await parentContract.methods @@ -72,6 +83,7 @@ describe('e2e_static_calls', () => { .send({ from: owner }); }); + // Parent routes through a second level of nesting before calling child's private read; asserts success. it('performs legal (nested) private to private static calls', async () => { await parentContract.methods .private_nested_static_call(childContract.address, await childContract.methods.private_get_value.selector(), [ @@ -81,6 +93,7 @@ describe('e2e_static_calls', () => { .send({ from: owner }); }); + // Parent calls child's public read-only function via low-level and typed interface; both succeed. it('performs legal public to public static calls', async () => { // Using low level calls await parentContract.methods @@ -91,12 +104,15 @@ describe('e2e_static_calls', () => { await parentContract.methods.public_get_value_from_child(childContract.address, 42n).send({ from: owner }); }); + // Parent routes through a second nesting level before calling child's public read; asserts success. it('performs legal (nested) public to public static calls', async () => { await parentContract.methods .public_nested_static_call(childContract.address, await childContract.methods.pub_get_value.selector(), [42n]) .send({ from: owner }); }); + // Parent enqueues a static public call to child's read function; asserts both low-level and typed + // interface variants succeed. it('performs legal enqueued public static calls', async () => { // Using low level calls await parentContract.methods @@ -111,6 +127,7 @@ describe('e2e_static_calls', () => { await parentContract.methods.enqueue_public_get_value_from_child(childContract.address, 42).send({ from: owner }); }); + // Enqueues a nested static public call through parent → child; asserts the tx succeeds. it('performs legal (nested) enqueued public static calls', async () => { await parentContract.methods .enqueue_static_nested_call_to_pub_function( @@ -121,6 +138,7 @@ describe('e2e_static_calls', () => { .send({ from: owner }); }); + // Parent makes a private static call to a state-mutating child function; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal private to private static calls', async () => { await expect( parentContract.methods @@ -133,6 +151,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Parent makes a non-static private call to a function that asserts static context; asserts STATIC_CONTEXT_ASSERTION_ERROR. it('fails when performing non-static calls to poorly written private static functions', async () => { await expect( parentContract.methods @@ -144,6 +163,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CONTEXT_ASSERTION_ERROR); }); + // Nested private static call from parent to a state-mutating child; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal (nested) private to private static calls', async () => { await expect( parentContract.methods @@ -156,6 +176,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Parent makes a public static call to a state-mutating child function; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal public to public static calls', async () => { await expect( parentContract.methods @@ -164,6 +185,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Nested public static call from parent to a state-mutating child; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal (nested) public to public static calls', async () => { await expect( parentContract.methods @@ -172,6 +194,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Parent enqueues a static public call to a state-mutating child function; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal enqueued public static calls', async () => { await expect( parentContract.methods @@ -184,6 +207,7 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Nested enqueued static call to a state-mutating function; asserts STATIC_CALL_STATE_MODIFICATION_ERROR. it('fails when performing illegal (nested) enqueued public static calls', async () => { await expect( parentContract.methods @@ -196,6 +220,8 @@ describe('e2e_static_calls', () => { ).rejects.toThrow(STATIC_CALL_STATE_MODIFICATION_ERROR); }); + // Parent enqueues a non-static call to a function that asserts it is called statically; asserts + // STATIC_CONTEXT_ASSERTION_ERROR. it('fails when performing non-static enqueue calls to poorly written public static functions', async () => { await expect( parentContract.methods diff --git a/yarn-project/end-to-end/src/e2e_storage_proof/e2e_storage_proof.test.ts b/yarn-project/end-to-end/src/e2e_storage_proof/e2e_storage_proof.test.ts index b3baf6a8ea67..399f5cee329b 100644 --- a/yarn-project/end-to-end/src/e2e_storage_proof/e2e_storage_proof.test.ts +++ b/yarn-project/end-to-end/src/e2e_storage_proof/e2e_storage_proof.test.ts @@ -8,6 +8,9 @@ import { buildStorageProofCapsules, loadStorageProofArgs } from './fixtures/stor jest.setTimeout(300_000); +// Tests that a Noir contract can verify an Ethereum storage proof (MPT proof) via oracle capsules. +// Plain setup(1, { ...AUTOMINE_E2E_OPTS }) with 1 account. Deploys StorageProofTestContract, then +// loads pre-computed proof args from fixtures/storage_proof.json and verifies on-chain. describe('Storage proof', () => { let ctx: EndToEndContext; let contract: StorageProofTestContract; @@ -21,6 +24,9 @@ describe('Storage proof', () => { await teardown(ctx); }); + // Loads pre-computed ethAddress/slotKey/slotContents/root from storage_proof.json, builds oracle + // capsules pointing to the contract, and calls contract.storage_proof() which verifies the MPT + // proof inside the circuit. Asserts execution succeeded. it('verifies a storage proof', async () => { const { ethAddress, slotKey, slotContents, root } = loadStorageProofArgs(); const capsules = await buildStorageProofCapsules(contract.address); diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index 3616e1385f7e..ac975cc652c5 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -311,6 +311,11 @@ const variants: VariantDefinition[] = [ { checkpointCount: 1000, txCount: 4, txComplexity: TxComplexity.PrivateTransfer }, ]; +// Sync stress-test and reorg-replay harness. The outer suite has two modes gated by env vars: +// AZTEC_GENERATE_TEST_DATA=1 builds fixture block data (slow, ~30-40 min), and the skipped inner +// describes replay that data for sync benchmarks and prune/reorg tests. Uses PIPELINING_SETUP_OPTS. +// All inner describe blocks are describe.skip and are not run in CI; only the outer it.each runs +// when AZTEC_GENERATE_TEST_DATA is set. describe('e2e_synching', () => { // WARNING: Running this with AZTEC_GENERATE_TEST_DATA is VERY slow, and will build a whole slew // of fixtures including multiple blocks with many transaction in. @@ -501,7 +506,11 @@ describe('e2e_synching', () => { await teardown(); }; + // Skipped in CI. Replays pre-generated fixture checkpoints via SequencerPublisher.enqueueProposeCheckpoint, + // then syncs a brand-new node and records the sync time. describe.skip('replay history and then do a fresh sync', () => { + // Replays all fixture checkpoints then creates a fresh AztecNodeService and times how long it takes + // to sync to the replayed chain tip; logs the result. it.each(variants)( 'vanilla - %s', async (variantDef: VariantDefinition) => { @@ -539,9 +548,14 @@ describe('e2e_synching', () => { ); }); + // Skipped in CI. Replays fixture checkpoints, then triggers a rollup prune via eth.warp and + // rollup.write.prune(); verifies that the archiver and world-state roll back correctly and that a + // node can re-extend the chain from the pruned tip. describe.skip('a wild prune appears', () => { const ASSUME_PROVEN_THROUGH = CheckpointNumber(0); + // Replays fixtures, then warps time and calls rollup.write.prune(); asserts the archiver drops + // the pruned blocks and world-state reverts to the proven checkpoint. it('archiver following catches reorg as it occur and deletes blocks', async () => { if (AZTEC_GENERATE_TEST_DATA) { return; @@ -636,6 +650,8 @@ describe('e2e_synching', () => { await rollup.write.prune(); // We need to sleep a bit to make sure that we have caught the prune and deleted blocks. + // REFACTOR: sleep-based wait for archiver to process the prune event; a retryUntil or event-based + // helper (waitForArchiverCheckpoint or similar) should replace this sleep. await sleep(3000); expect(await archiver.getCheckpointNumber()).toBe(provenThrough); @@ -665,6 +681,8 @@ describe('e2e_synching', () => { ); }); + // Replays fixtures, marks partial chain as proven, warps time and prunes; asserts a synced node + // sees a lower block number after prune, then extends the chain with new txs. it('node following prunes and can extend chain (fresh pxe)', async () => { // @todo this should be rewritten slightly when the PXE can handle re-orgs // such that it does not need to be run "fresh" Issue #9327 @@ -700,6 +718,8 @@ describe('e2e_synching', () => { hash: await rollup.write.prune(), }); + // REFACTOR: sleep-based wait for node to process prune and update block number; a retryUntil + // waiting on getBlockNumber() < blockBeforePrune should replace this sleep. await sleep(5000); expect(await aztecNode.getBlockNumber()).toBeLessThan(blockBeforePrune); @@ -725,6 +745,8 @@ describe('e2e_synching', () => { ); }); + // Replays fixtures, marks partial chain as proven, warps time and prunes, then syncs a brand-new + // node; asserts the new node can extend the pruned chain with fresh txs. it('fresh sync can extend chain', async () => { if (AZTEC_GENERATE_TEST_DATA) { return; diff --git a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts index bf1008cf4510..0c4553a044b7 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/access_control.test.ts @@ -1,6 +1,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers admin and minter role management on the Token contract: set_admin, set_minter, and failure cases +// when called by a non-admin. Setup: single node with AutomineSequencer (AUTOMINE_E2E_OPTS), 3 accounts +// deployed, Token contract deployed. No time-warp needed (Token has no role-change delay). describe('e2e_token_contract access control', () => { const t = new TokenContractTest('access_control'); @@ -17,6 +20,7 @@ describe('e2e_token_contract access control', () => { await t.tokenSim.check(); }); + // Sets account1 as the new admin via set_admin, then reads back the admin via get_admin. it('Set admin', async () => { await t.asset.methods.set_admin(t.account1Address).send({ from: t.adminAddress }); expect((await t.asset.methods.get_admin().simulate({ from: t.adminAddress })).result).toBe( @@ -24,22 +28,27 @@ describe('e2e_token_contract access control', () => { ); }); + // Grants minter role to account1 (now admin) via set_minter(true) and verifies via is_minter. it('Add minter as admin', async () => { await t.asset.methods.set_minter(t.account1Address, true).send({ from: t.account1Address }); expect((await t.asset.methods.is_minter(t.account1Address).simulate({ from: t.adminAddress })).result).toBe(true); }); + // Revokes minter role from account1 via set_minter(false) and verifies via is_minter. it('Revoke minter as admin', async () => { await t.asset.methods.set_minter(t.account1Address, false).send({ from: t.account1Address }); expect((await t.asset.methods.is_minter(t.account1Address).simulate({ from: t.adminAddress })).result).toBe(false); }); + // Error cases: unauthorized set_admin and unauthorized set_minter. describe('failure cases', () => { + // Attempts set_admin from the original admin address (which is no longer admin); expects 'caller is not admin'. it('Set admin (not admin)', async () => { await expect(t.asset.methods.set_admin(t.adminAddress).simulate({ from: t.adminAddress })).rejects.toThrow( 'Assertion failed: caller is not admin', ); }); + // Attempts set_minter from the original admin address (which is no longer admin); expects 'caller is not admin'. it('Revoke minter not as admin', async () => { await expect( t.asset.methods.set_minter(t.adminAddress, false).simulate({ from: t.adminAddress }), diff --git a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts index c256dc91e27d..0540453ab413 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/burn.test.ts @@ -5,6 +5,8 @@ import { sendThroughAuthwitProxy, simulateThroughAuthwitProxy } from '../fixture import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR, U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers public and private burn on Token contract: direct, authwit-delegated via proxy, and error paths. +// Setup: single node with AutomineSequencer, 3 accounts, Token deployed with initial public and private mint. describe('e2e_token_contract burn', () => { const t = new TokenContractTest('burn'); let { asset, tokenSim, wallet, adminAddress, account1Address } = t; @@ -25,7 +27,9 @@ describe('e2e_token_contract burn', () => { await t.tokenSim.check(); }); + // Public burn: direct burn, authwit-delegated burn, and error cases. describe('public', () => { + // Burns half the admin's public balance and verifies via TokenSimulator. it('burn less than balance', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -35,6 +39,8 @@ describe('e2e_token_contract burn', () => { tokenSim.burnPublic(adminAddress, amount); }); + // Grants a public authwit for burn to account1, burns, verifies TokenSimulator, then confirms replay + // reverts with unauthorized. it('burn on behalf of other', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -59,7 +65,9 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow(/unauthorized/); }); + // Error paths for public burn. describe('failure cases', () => { + // Attempts to burn more than public balance; expects U128_UNDERFLOW_ERROR. it('burn more than balance', async () => { const { result: balance0 } = await asset.methods .balance_of_public(adminAddress) @@ -71,6 +79,7 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Self-burn with nonce=1; expects the invalid-nonce assertion. it('burn on behalf of self with non-zero nonce', async () => { const { result: balance0 } = await asset.methods .balance_of_public(adminAddress) @@ -85,6 +94,7 @@ describe('e2e_token_contract burn', () => { ); }); + // Burn from account1 without authwit; expects unauthorized. it('burn on behalf of other without "approval"', async () => { const { result: balance0 } = await asset.methods .balance_of_public(adminAddress) @@ -96,6 +106,7 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow(/unauthorized/); }); + // Approves a burn exceeding balance via authwit; expects U128_UNDERFLOW_ERROR on simulate. it('burn more than balance on behalf of other', async () => { const { result: balance0 } = await asset.methods .balance_of_public(adminAddress) @@ -116,6 +127,7 @@ describe('e2e_token_contract burn', () => { await expect(action.simulate({ from: account1Address })).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Approves adminAddress as caller but tries from account1; expects unauthorized. it('burn on behalf of other, wrong designated caller', async () => { const { result: balance0 } = await asset.methods .balance_of_public(adminAddress) @@ -140,7 +152,9 @@ describe('e2e_token_contract burn', () => { }); }); + // Private burn: direct burn, authwit-delegated burn via proxy, and error cases. describe('private', () => { + // Burns half the admin's private balance and verifies via TokenSimulator. it('burn less than balance', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -151,6 +165,8 @@ describe('e2e_token_contract burn', () => { tokenSim.burnPrivate(adminAddress, amount); }); + // Creates a private authwit for burn_private, sends through proxy, verifies TokenSimulator, then asserts + // replay fails with DUPLICATE_NULLIFIER_ERROR. it('burn on behalf of other', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -172,7 +188,9 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths for private burn. describe('failure cases', () => { + // Attempts to burn more than private balance; expects 'Balance too low'. it('burn more than balance', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -184,6 +202,7 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Self-burn with nonce=1; expects the invalid-nonce assertion. it('burn on behalf of self with non-zero nonce', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -197,6 +216,7 @@ describe('e2e_token_contract burn', () => { ); }); + // Creates authwit for burn exceeding balance via proxy; expects 'Balance too low' on simulate. it('burn more than balance on behalf of other', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -214,6 +234,7 @@ describe('e2e_token_contract burn', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Simulates burn through proxy without a witness; expects unknown-authwit error. it('burn on behalf of other without approval', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -235,6 +256,8 @@ describe('e2e_token_contract burn', () => { ); }); + // Creates authwit designating account1 as caller but sends through proxy; expects unknown-authwit error + // because the message hash references the proxy, not account1. it('on behalf of other (invalid designated caller)', async () => { const { result: balancePriv0 } = await asset.methods .balance_of_private(adminAddress) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts index 75320121fa15..43449b6a1abd 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/minting.test.ts @@ -1,6 +1,8 @@ import { AUTOMINE_E2E_OPTS, U128_OVERFLOW_ERROR } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers public and private minting on Token contract, including minter role enforcement and overflow checks. +// Setup: single node with AutomineSequencer, 3 accounts deployed, Token contract deployed (no initial mint). describe('e2e_token_contract minting', () => { const t = new TokenContractTest('minting'); let { asset, tokenSim, adminAddress, account1Address } = t; @@ -19,7 +21,9 @@ describe('e2e_token_contract minting', () => { await t.tokenSim.check(); }); + // Public mint path: success and overflow/permission failure cases. describe('Public', () => { + // Mints 10000 tokens publicly as admin-minter and verifies balance and total supply via TokenSimulator. it('as minter', async () => { const amount = 10000n; await asset.methods.mint_to_public(adminAddress, amount).send({ from: adminAddress }); @@ -33,7 +37,9 @@ describe('e2e_token_contract minting', () => { ); }); + // Error paths for public mint. describe('failure cases', () => { + // Attempts mint_to_public from account1 (not a minter); expects 'caller is not minter'. it('as non-minter', async () => { const amount = 10000n; await expect( @@ -41,6 +47,7 @@ describe('e2e_token_contract minting', () => { ).rejects.toThrow('Assertion failed: caller is not minter'); }); + // Mints an amount that would overflow the recipient's u128 public balance; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPublic(adminAddress); await expect( @@ -48,6 +55,7 @@ describe('e2e_token_contract minting', () => { ).rejects.toThrow(U128_OVERFLOW_ERROR); }); + // Mints an amount that would overflow total supply across accounts; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPublic(adminAddress); await expect( @@ -57,7 +65,9 @@ describe('e2e_token_contract minting', () => { }); }); + // Private mint path: success and overflow/permission failure cases. describe('Private', () => { + // Mints 10000 tokens privately as admin-minter and verifies balance and total supply via TokenSimulator. it('as minter', async () => { const amount = 10000n; await asset.methods.mint_to_private(adminAddress, amount).send({ from: adminAddress }); @@ -71,7 +81,9 @@ describe('e2e_token_contract minting', () => { ); }); + // Error paths for private mint. describe('failure cases', () => { + // Attempts mint_to_private from account1 (not a minter); expects 'caller is not minter'. it('as non-minter', async () => { const amount = 10000n; await expect( @@ -79,7 +91,7 @@ describe('e2e_token_contract minting', () => { ).rejects.toThrow('Assertion failed: caller is not minter'); }); - // This test is expected to fail at the ABI encoder rather than during contract logic. + // Passes an overflowed u128 to mint_to_private; expected to fail at ABI encoding, not contract logic. // We keep the test to be defensive as it is the only e2e test with overflowed inputs. it('mint >u128 tokens to overflow', async () => { const overflowAmount = 2n ** 128n; @@ -88,6 +100,7 @@ describe('e2e_token_contract minting', () => { ).rejects.toThrow('does not fit in u128'); }); + // Mints an amount that would overflow the recipient's private u128 balance; expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPrivate(adminAddress); await expect( @@ -95,6 +108,7 @@ describe('e2e_token_contract minting', () => { ).rejects.toThrow(U128_OVERFLOW_ERROR); }); + // Mints an amount that would overflow total supply (private path); expects U128_OVERFLOW_ERROR. it('mint u128', async () => { const amount = 2n ** 128n - tokenSim.balanceOfPrivate(adminAddress); await expect( diff --git a/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts index ee4b057ffb84..5143ea0f93e3 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/private_transfer_recursion.test.ts @@ -5,6 +5,9 @@ import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { mintNotes } from '../fixtures/token_utils.js'; import { TokenContractTest } from './token_contract_test.js'; +// Verifies that the Token contract's private transfer function correctly handles note consolidation across +// recursive calls (consuming many notes via two levels of recursion). Also checks that private Transfer +// events are emitted and readable. Setup: single node with AutomineSequencer, 3 accounts, Token deployed. describe('e2e_token_contract private transfer recursion', () => { const t = new TokenContractTest('odd_transfer_private'); let { asset, wallet, adminAddress, account1Address, node } = t; @@ -19,6 +22,8 @@ describe('e2e_token_contract private transfer recursion', () => { await t.teardown(); }); + // Mints 16 separate notes of 10 tokens each, transfers the full balance to account1, then verifies that + // all 16 notes were nullified, one new note was created, and the Transfer event is readable. it('transfer full balance', async () => { // We insert 16 notes, which is large enough to guarantee that the token will need to do two recursive calls to // itself to consume them all (since it retrieves 2 notes on the first pass and 8 in each subsequent pass). @@ -55,6 +60,9 @@ describe('e2e_token_contract private transfer recursion', () => { }); }); + // Mints 4 notes, transfers less than the total (forcing partial note use), and verifies that the correct + // number of nullifiers and note hashes are produced, the sender has the expected change, and the Transfer + // event is readable. it('transfer less than full balance and get change', async () => { const noteAmounts = [10n, 10n, 10n, 10n]; const expectedChange = 3n; // This will result in one of the notes being partially used @@ -96,7 +104,9 @@ describe('e2e_token_contract private transfer recursion', () => { }); }); + // Error path for recursive private transfer. describe('failure cases', () => { + // Attempts to transfer more than the available private balance; expects 'Balance too low'. it('transfer more than balance', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts index b58142beb64c..a74a28ca8d40 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/reading_constants.test.ts @@ -3,6 +3,9 @@ import { readFieldCompressedString } from '@aztec/aztec.js/utils'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Verifies that Token contract constants (name, symbol, decimals) are readable from both private and public +// entry points and match the values supplied at deploy time. Setup: single node with AutomineSequencer, +// Token contract deployed with TOKEN_NAME/SYMBOL/DECIMALS. describe('e2e_token_contract reading constants', () => { const t = new TokenContractTest('reading_constants'); const { TOKEN_DECIMALS, TOKEN_NAME, TOKEN_SYMBOL } = TokenContractTest; @@ -22,6 +25,7 @@ describe('e2e_token_contract reading constants', () => { await t.tokenSim.check(); }); + // Calls private_get_name via simulate and asserts it equals TOKEN_NAME after decoding the compressed string. it('check name private', async () => { const name = readFieldCompressedString( (await t.asset.methods.private_get_name().simulate({ from: t.adminAddress })).result, @@ -29,6 +33,7 @@ describe('e2e_token_contract reading constants', () => { expect(name).toBe(TOKEN_NAME); }); + // Calls public_get_name via simulate and asserts it equals TOKEN_NAME after decoding. it('check name public', async () => { const name = readFieldCompressedString( (await t.asset.methods.public_get_name().simulate({ from: t.adminAddress })).result, @@ -36,6 +41,7 @@ describe('e2e_token_contract reading constants', () => { expect(name).toBe(TOKEN_NAME); }); + // Calls private_get_symbol via simulate and asserts it equals TOKEN_SYMBOL after decoding. it('check symbol private', async () => { const sym = readFieldCompressedString( (await t.asset.methods.private_get_symbol().simulate({ from: t.adminAddress })).result, @@ -43,6 +49,7 @@ describe('e2e_token_contract reading constants', () => { expect(sym).toBe(TOKEN_SYMBOL); }); + // Calls public_get_symbol via simulate and asserts it equals TOKEN_SYMBOL after decoding. it('check symbol public', async () => { const sym = readFieldCompressedString( (await t.asset.methods.public_get_symbol().simulate({ from: t.adminAddress })).result, @@ -50,11 +57,13 @@ describe('e2e_token_contract reading constants', () => { expect(sym).toBe(TOKEN_SYMBOL); }); + // Calls private_get_decimals via simulate and asserts it equals TOKEN_DECIMALS (18n). it('check decimals private', async () => { const { result: dec } = await t.asset.methods.private_get_decimals().simulate({ from: t.adminAddress }); expect(dec).toBe(TOKEN_DECIMALS); }); + // Calls public_get_decimals via simulate and asserts it equals TOKEN_DECIMALS (18n). it('check decimals public', async () => { const { result: dec } = await t.asset.methods.public_get_decimals().simulate({ from: t.adminAddress }); expect(dec).toBe(TOKEN_DECIMALS); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts index 900fb20af049..bca964bc8762 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer.test.ts @@ -5,6 +5,10 @@ import { TokenContract, type Transfer } from '@aztec/noir-contracts.js/Token'; import { AUTOMINE_E2E_OPTS } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers the top-level transfer() entry point on Token contract (private-to-private), including transfer to +// non-deployed accounts and private Transfer event emission. Note: the describe title collides with +// transfer_in_private.test.ts — the tested contract methods differ (transfer vs transfer_in_private). +// Setup: single node with AutomineSequencer, 3 accounts, Token deployed with initial mint. describe('e2e_token_contract transfer private', () => { const t = new TokenContractTest('transfer_private'); let { asset, adminAddress, wallet, account1Address, tokenSim } = t; @@ -24,6 +28,8 @@ describe('e2e_token_contract transfer private', () => { await t.tokenSim.check(); }); + // Transfers half of admin's private balance to account1, verifies via TokenSimulator, and asserts that + // the private Transfer event is emitted and readable in the recipient's scope. it('transfer less than balance', async () => { const { result: balance0 } = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -53,6 +59,8 @@ describe('e2e_token_contract transfer private', () => { }); }); + // Transfers to a randomly generated non-deployed address. Because the recipient's keys aren't in the PXE, + // the note can't be decrypted; TokenSimulator models this as a transfer to AztecAddress.ZERO. it('transfer less than balance to non-deployed account', async () => { const { result: balance0 } = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -68,6 +76,7 @@ describe('e2e_token_contract transfer private', () => { tokenSim.transferPrivate(adminAddress, AztecAddress.ZERO, amount); }); + // Transfers half of admin's balance to themselves and verifies the balance is unchanged. it('transfer to self', async () => { const { result: balance0 } = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -76,7 +85,9 @@ describe('e2e_token_contract transfer private', () => { tokenSim.transferPrivate(adminAddress, adminAddress, amount); }); + // Error paths for transfer(). describe('failure cases', () => { + // Attempts to transfer more than private balance; expects 'Balance too low'. it('transfer more than balance', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_private.test.ts index 676285ea1b3f..2c2773a46126 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_private.test.ts @@ -5,6 +5,9 @@ import { sendThroughAuthwitProxy, simulateThroughAuthwitProxy } from '../fixture import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers the transfer_in_private entry point on Token contract: authwit-delegated transfers via proxy, +// authwit cancellation, and error paths including bad-account validation. Setup: single node with +// AutomineSequencer, 3 accounts + InvalidAccount, Token deployed with initial mint. describe('e2e_token_contract transfer private', () => { const t = new TokenContractTest('transfer_private'); let { asset, tokenSim, wallet, adminAddress, account1Address, badAccount } = t; @@ -24,6 +27,8 @@ describe('e2e_token_contract transfer private', () => { await t.tokenSim.check(); }); + // Creates a private authwit for transfer_in_private, sends through proxy, verifies TokenSimulator, + // then confirms replay reverts with DUPLICATE_NULLIFIER_ERROR. it('transfer on behalf of other', async () => { const { result: balance0 } = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -43,7 +48,11 @@ describe('e2e_token_contract transfer private', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths: invalid nonce, over-balance via authwit, no approval, wrong caller, cancelled authwit, + // and invalid verify_private_authwit from a bad account contract. describe('failure cases', () => { + // Self-transfer via transfer_in_private with nonce=1; expects the invalid-nonce assertion with a stack + // trace matching Token.transfer_in_private. it('transfer on behalf of self with non-zero nonce', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -62,6 +71,8 @@ describe('e2e_token_contract transfer private', () => { ); }); + // Authwit-transfers more than private balance via proxy; expects 'Balance too low' and verifies balances + // unchanged. it('transfer more than balance on behalf of other', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -95,6 +106,7 @@ describe('e2e_token_contract transfer private', () => { // See https://github.com/AztecProtocol/aztec-packages/issues/1259 }); + // Simulates transfer_in_private through proxy without a witness; expects unknown-authwit error. it('transfer on behalf of other without approval', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -116,6 +128,7 @@ describe('e2e_token_contract transfer private', () => { ); }); + // Creates authwit designating account1 as caller but sends through proxy; expects unknown-authwit error. it('transfer on behalf of other, wrong designated caller', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -142,6 +155,8 @@ describe('e2e_token_contract transfer private', () => { ); }); + // Creates a private authwit, cancels it via cancel_authwit (which nullifies the inner hash), then + // attempts the transfer through proxy and expects DUPLICATE_NULLIFIER_ERROR. it('transfer on behalf of other, cancelled authwit', async () => { const { result: balance0 } = await asset.methods .balance_of_private(adminAddress) @@ -166,6 +181,8 @@ describe('e2e_token_contract transfer private', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Uses the InvalidAccount contract as the 'from' address; expects 'Message not authorized by account' + // because the bad contract returns a malformed validation response. it('transfer on behalf of other, invalid verify_private_authwit on "from"', async () => { const authwitNonce = Fr.random(); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_public.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_public.test.ts index 550d34fbe0b3..689736e3facf 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_in_public.test.ts @@ -18,6 +18,9 @@ const qosAlerts: AlertConfig[] = [ }, ]; +// Covers the transfer_in_public entry point on Token contract: direct, self, authwit-delegated, authwit +// cancellation (two flows), and bad-account validation. Also conditionally checks Grafana QoS alerts when +// CHECK_ALERTS=true. Setup: single node with AutomineSequencer, Token deployed with initial mint. describe('e2e_token_contract transfer public', () => { const t = new TokenContractTest('transfer_in_public'); let { asset, tokenSim, wallet, adminAddress, account1Address, badAccount } = t; @@ -42,6 +45,7 @@ describe('e2e_token_contract transfer public', () => { await t.tokenSim.check(); }); + // Transfers half of admin's public balance to account1 and verifies via TokenSimulator. it('transfer less than balance', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -51,6 +55,7 @@ describe('e2e_token_contract transfer public', () => { tokenSim.transferPublic(adminAddress, account1Address, amount); }); + // Transfers half of admin's public balance to themselves; verifies balance is unchanged via TokenSimulator. it('transfer to self', async () => { const { result: balance } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance / 2n; @@ -60,6 +65,8 @@ describe('e2e_token_contract transfer public', () => { tokenSim.transferPublic(adminAddress, adminAddress, amount); }); + // Sets a public authwit allowing account1 to transfer admin's tokens, executes, verifies TokenSimulator, + // then confirms replay reverts with unauthorized. it('transfer on behalf of other', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -88,7 +95,10 @@ describe('e2e_token_contract transfer public', () => { ).rejects.toThrow(/unauthorized/); }); + // Error paths for transfer_in_public: overflow, nonce, no approval, over-balance via authwit, wrong + // caller (two variants), authwit cancellation (two flows), and bad-account authwit validation. describe('failure cases', () => { + // Attempts to transfer more than public balance; expects U128_UNDERFLOW_ERROR. it('transfer more than balance', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 + 1n; @@ -100,6 +110,7 @@ describe('e2e_token_contract transfer public', () => { ).rejects.toThrow(U128_UNDERFLOW_ERROR); }); + // Self-transfer with nonce=1; expects the invalid-nonce assertion. it('transfer on behalf of self with non-zero nonce', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 - 1n; @@ -113,6 +124,7 @@ describe('e2e_token_contract transfer public', () => { ); }); + // Calls transfer_in_public from account1 without an authwit; expects unauthorized. it('transfer on behalf of other without "approval"', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 + 1n; @@ -124,6 +136,8 @@ describe('e2e_token_contract transfer public', () => { ).rejects.toThrow(/unauthorized/); }); + // Approves a transfer exceeding balance via authwit; expects U128_UNDERFLOW_ERROR and verifies balances + // unchanged. it('transfer more than balance on behalf of other', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const { result: balance1 } = await asset.methods @@ -155,6 +169,7 @@ describe('e2e_token_contract transfer public', () => { ).toEqual(balance1); }); + // Approves adminAddress as caller but executes from account1; expects unauthorized, balances unchanged. it('transfer on behalf of other, wrong designated caller', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const { result: balance1 } = await asset.methods @@ -185,6 +200,8 @@ describe('e2e_token_contract transfer public', () => { ).toEqual(balance1); }); + // Duplicate of the preceding test — identical logic and title, likely a test authoring mistake. + // Approves adminAddress as caller but executes from account1; expects unauthorized, balances unchanged. it('transfer on behalf of other, wrong designated caller', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const { result: balance1 } = await asset.methods @@ -214,6 +231,8 @@ describe('e2e_token_contract transfer public', () => { ).toEqual(balance1); }); + // Grants a public authwit to account1, then revokes it via setPublicAuthWit(false), then confirms + // the transfer simulation reverts with unauthorized (uses fixed method call form for simulate). it('transfer on behalf of other, cancelled authwit', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -243,6 +262,8 @@ describe('e2e_token_contract transfer public', () => { ).rejects.toThrow(/unauthorized/); }); + // Same grant-and-revoke flow as 'cancelled authwit' but simulates via the action object directly rather + // than re-constructing the method call — verifies both call forms produce unauthorized. it('transfer on behalf of other, cancelled authwit, flow 2', async () => { const { result: balance0 } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balance0 / 2n; @@ -268,6 +289,8 @@ describe('e2e_token_contract transfer public', () => { await expect(action.simulate({ from: account1Address })).rejects.toThrow(/unauthorized/); }); + // Uses the InvalidAccount contract as the 'from' address; expects unauthorized because the bad contract + // returns a malformed authwit validation value. it('transfer on behalf of other, invalid spend_public_authwit on "from"', async () => { const authwitNonce = Fr.random(); diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts index 03bf880e7318..e9de4064a1f9 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_private.test.ts @@ -1,6 +1,9 @@ import { AUTOMINE_E2E_OPTS, U128_UNDERFLOW_ERROR } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers the transfer_to_private entry point on Token contract (public→private), including self and +// cross-account transfers. Setup: single node with AutomineSequencer, 3 accounts, Token deployed with +// initial mint. describe('e2e_token_contract transfer_to_private', () => { const t = new TokenContractTest('transfer_to_private'); let { asset, adminAddress, account1Address, tokenSim } = t; @@ -21,6 +24,7 @@ describe('e2e_token_contract transfer_to_private', () => { await t.tokenSim.check(); }); + // Transfers half of admin's public balance to admin's own private balance and verifies via TokenSimulator. it('to self', async () => { const { result: balancePub } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balancePub / 2n; @@ -33,6 +37,7 @@ describe('e2e_token_contract transfer_to_private', () => { await tokenSim.check(); }); + // Transfers half of admin's public balance to account1's private balance and verifies via TokenSimulator. it('to someone else', async () => { const { result: balancePub } = await asset.methods.balance_of_public(adminAddress).simulate({ from: adminAddress }); const amount = balancePub / 2n; @@ -45,7 +50,9 @@ describe('e2e_token_contract transfer_to_private', () => { await tokenSim.check(); }); + // Error paths for transfer_to_private. describe('failure cases', () => { + // Attempts to transfer more than public balance to private; expects U128_UNDERFLOW_ERROR. it('to self (more than balance)', async () => { const { result: balancePub } = await asset.methods .balance_of_public(adminAddress) diff --git a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_public.test.ts b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_public.test.ts index debb3f2b0234..362666dd76ec 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_public.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract/transfer_to_public.test.ts @@ -5,6 +5,9 @@ import { sendThroughAuthwitProxy, simulateThroughAuthwitProxy } from '../fixture import { AUTOMINE_E2E_OPTS, DUPLICATE_NULLIFIER_ERROR } from '../fixtures/fixtures.js'; import { TokenContractTest } from './token_contract_test.js'; +// Covers the transfer_to_public entry point on Token contract (private→public): direct, authwit-delegated +// via proxy, and error paths. Setup: single node with AutomineSequencer, 3 accounts, Token deployed with +// initial mint. describe('e2e_token_contract transfer_to_public', () => { const t = new TokenContractTest('transfer_to_public'); let { asset, wallet, adminAddress, account1Address, tokenSim } = t; @@ -25,6 +28,7 @@ describe('e2e_token_contract transfer_to_public', () => { await t.tokenSim.check(); }); + // Transfers half of admin's private balance to admin's public balance and verifies via TokenSimulator. it('on behalf of self', async () => { const { result: balancePriv } = await asset.methods .balance_of_private(adminAddress) @@ -37,6 +41,8 @@ describe('e2e_token_contract transfer_to_public', () => { tokenSim.transferToPublic(adminAddress, adminAddress, amount); }); + // Creates a private authwit for transfer_to_public to account1, sends through proxy, verifies TokenSimulator, + // then asserts replay reverts with DUPLICATE_NULLIFIER_ERROR. it('on behalf of other', async () => { const { result: balancePriv0 } = await asset.methods .balance_of_private(adminAddress) @@ -58,7 +64,9 @@ describe('e2e_token_contract transfer_to_public', () => { ).rejects.toThrow(DUPLICATE_NULLIFIER_ERROR); }); + // Error paths: more-than-balance, invalid nonce, over-balance via authwit, wrong caller. describe('failure cases', () => { + // Transfers more than private balance to public (self); expects 'Balance too low'. it('on behalf of self (more than balance)', async () => { const { result: balancePriv } = await asset.methods .balance_of_private(adminAddress) @@ -71,6 +79,7 @@ describe('e2e_token_contract transfer_to_public', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Self-transfer_to_public with nonce=1; expects the invalid-nonce assertion. it('on behalf of self (invalid authwit nonce)', async () => { const { result: balancePriv } = await asset.methods .balance_of_private(adminAddress) @@ -85,6 +94,7 @@ describe('e2e_token_contract transfer_to_public', () => { ); }); + // Creates authwit for a transfer_to_public exceeding private balance; expects 'Balance too low'. it('on behalf of other (more than balance)', async () => { const { result: balancePriv0 } = await asset.methods .balance_of_private(adminAddress) @@ -102,6 +112,7 @@ describe('e2e_token_contract transfer_to_public', () => { ).rejects.toThrow('Assertion failed: Balance too low'); }); + // Creates authwit designating account1 as caller but sends through proxy; expects unknown-authwit error. it('on behalf of other (invalid designated caller)', async () => { const { result: balancePriv0 } = await asset.methods .balance_of_private(adminAddress) diff --git a/yarn-project/end-to-end/src/e2e_tx_effect_oracle.test.ts b/yarn-project/end-to-end/src/e2e_tx_effect_oracle.test.ts index 907439f12a75..78764f52f1d1 100644 --- a/yarn-project/end-to-end/src/e2e_tx_effect_oracle.test.ts +++ b/yarn-project/end-to-end/src/e2e_tx_effect_oracle.test.ts @@ -32,7 +32,8 @@ const TIMEOUT = 120_000; // Note: This would ideally be an integration test as it only tests an oracle implementation but we currently don't // have the infrastructure for that. Testing it with TXE is also infeasible as there we would not be able to check the // obtained tx effect from oracle with the one obtained directly in TS. - +// +// Uses a single node with AutomineSequencer and one account. describe('e2e tx effect oracle', () => { let contract: TxEffectOracleTestContract; let deployTxHash: TxHash; @@ -59,6 +60,8 @@ describe('e2e tx effect oracle', () => { afterAll(() => teardown()); + // Fetches the deploy tx's TxEffect from the node, computes a poseidon2 hash over the padded field + // layout in TS, then simulates the Noir contract's get_tx_effect_hash and compares the two hashes. it('tx effect in Noir exactly matches tx effect in TS', async () => { const nodeEffect = await aztecNode.getTxEffect(deployTxHash); expect(nodeEffect).toBeDefined(); @@ -73,6 +76,8 @@ describe('e2e tx effect oracle', () => { expect(actualHash).toEqual(expectedHash.toBigInt()); }); + // Passes a random Fr as a tx hash to assert_is_none; asserts the oracle returns None (the tx does + // not exist) without throwing. it('aztec_utl_getTxEffect oracle returns None for a random tx hash', async () => { await contract.methods.assert_is_none(Fr.random()).simulate({ from: defaultAccountAddress }); }); diff --git a/yarn-project/end-to-end/src/forward-compatibility/e2e_amm.test.ts b/yarn-project/end-to-end/src/forward-compatibility/e2e_amm.test.ts index d5c86371874b..db20ff75065b 100644 --- a/yarn-project/end-to-end/src/forward-compatibility/e2e_amm.test.ts +++ b/yarn-project/end-to-end/src/forward-compatibility/e2e_amm.test.ts @@ -38,6 +38,9 @@ const TIMEOUT = 300_000; const { REMOTE_WALLET_URL = 'http://localhost:8081' } = process.env; +// Forward-compatibility AMM test. Connects to a wallet service started from an older Aztec release +// (REMOTE_WALLET_URL) via JSON-RPC, then deploys and exercises AMM contracts compiled with the current +// Noir version. CI-excluded; requires both release binaries to be available. describe('forward-compatibility: AMM', () => { jest.setTimeout(TIMEOUT); diff --git a/yarn-project/end-to-end/src/guides/up_quick_start.test.ts b/yarn-project/end-to-end/src/guides/up_quick_start.test.ts index b1fd793d993c..036e2b2e1145 100644 --- a/yarn-project/end-to-end/src/guides/up_quick_start.test.ts +++ b/yarn-project/end-to-end/src/guides/up_quick_start.test.ts @@ -4,7 +4,9 @@ import { execSync } from 'child_process'; const { AZTEC_NODE_URL = 'http://localhost:8080' } = process.env; -// Entrypoint for running the up-quick-start script on the CI +// Entrypoint for running the up-quick-start script on the CI. +// Connects to AZTEC_NODE_URL (pre-started docker-compose network) then shells out to up_quick_start.sh. +// Requires a running compose stack; no in-proc setup(). describe('guides/up_quick_start', () => { // TODO: update to not use CLI it('works', async () => { diff --git a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts index b49e09322174..8eae8db18827 100644 --- a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts +++ b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts @@ -41,6 +41,9 @@ class SchnorrHardcodedKeyAccountContract extends DefaultAccountContract { } } +// Guide-level test demonstrating a custom Schnorr account contract implementation. Uses setup() with +// AUTOMINE_E2E_OPTS; runs fully in-proc. Sits in the compose suite by glob inclusion, not by need — +// migrate-later candidate for the single-node category. describe('guides/writing_an_account_contract', () => { let context: Awaited>; diff --git a/yarn-project/end-to-end/src/spartan/1tps.test.ts b/yarn-project/end-to-end/src/spartan/1tps.test.ts index 6d939deb5886..3e18f925784e 100644 --- a/yarn-project/end-to-end/src/spartan/1tps.test.ts +++ b/yarn-project/end-to-end/src/spartan/1tps.test.ts @@ -21,6 +21,8 @@ import { import { type ServiceEndpoint, getRPCEndpoint, setupEnvironment } from './utils.js'; const config = { ...setupEnvironment(process.env) }; +// 1 TPS sustained transfer test against a live k8s deployment. Not yet wired into CI (see TODO above). +// Sends token transfers at roughly 1 tx/s and verifies inclusion. describe('token transfer test', () => { jest.setTimeout(10 * 60 * 2000); // 20 minutes diff --git a/yarn-project/end-to-end/src/spartan/4epochs.test.ts b/yarn-project/end-to-end/src/spartan/4epochs.test.ts index a0d42328fb7a..792b1cce2025 100644 --- a/yarn-project/end-to-end/src/spartan/4epochs.test.ts +++ b/yarn-project/end-to-end/src/spartan/4epochs.test.ts @@ -21,6 +21,9 @@ import { ChainHealth, type ServiceEndpoint, getEthereumEndpoint, getRPCEndpoint, const config = { ...setupEnvironment(process.env) }; +// Sustained token transfer load across 4 full epochs against a live k8s deployment. Verifies that +// the cluster can advance through multiple epoch boundaries while continuously processing transfers, +// and that the proven chain advances past the 4-epoch mark. describe('token transfer test', () => { jest.setTimeout(10 * 60 * 4000); // 40 minutes 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 index 26f6eb711f9f..8a3fcbc5c3e2 100644 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -54,6 +54,8 @@ const TOKEN_TESTS = [ const maxTxs = Math.max(...[...BENCH_TESTS, ...TOKEN_TESTS].map(t => t[1])); const NUM_WALLETS = txRealProofs ? Math.min(10, maxTxs) : 1; +// Block capacity benchmark against a live k8s deployment. Fills blocks with up to 100 transactions per +// type (noop, nullifier emission, note emission, etc.) and measures inclusion; outputs benchmark JSON. describe('block capacity benchmark', () => { jest.setTimeout(60 * 60 * 1000); // 60 minutes diff --git a/yarn-project/end-to-end/src/spartan/gating-passive.test.ts b/yarn-project/end-to-end/src/spartan/gating-passive.test.ts index 4a8bf19ae4a7..1d479d236fac 100644 --- a/yarn-project/end-to-end/src/spartan/gating-passive.test.ts +++ b/yarn-project/end-to-end/src/spartan/gating-passive.test.ts @@ -49,6 +49,9 @@ const config = setupEnvironment(process.env); const { NAMESPACE } = config; const debugLogger = createLogger('e2e:spartan-test:gating-passive'); +// Passive network gating test against a live k8s deployment. Applies Chaos Mesh network shaping and a +// boot-node failure, starts a transfer bot, and evaluates Grafana/Prometheus alert rules to ensure +// sequencer attestation timing and archiver sync rates stay within acceptable bounds. describe('a test that passively observes the network in the presence of network chaos', () => { jest.setTimeout(60 * 60 * 1000); // 60 minutes diff --git a/yarn-project/end-to-end/src/spartan/mbps.test.ts b/yarn-project/end-to-end/src/spartan/mbps.test.ts index da682f5fb489..bc1fa4fac1ff 100644 --- a/yarn-project/end-to-end/src/spartan/mbps.test.ts +++ b/yarn-project/end-to-end/src/spartan/mbps.test.ts @@ -30,6 +30,9 @@ import type { ServiceEndpoint } from './utils.js'; const config = setupEnvironment(process.env); +// Multi-blocks-per-slot (MBPS) test against a live k8s deployment. Reconfigures sequencers to allow +// multiple blocks per L2 slot and verifies that the rollup contract reports more than one block produced +// within a single slot period. describe('multi-blocks-per-slot network test', () => { jest.setTimeout(60 * 60 * 1000); // 60 minutes diff --git a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts index 752946fa34ff..92dcbba12e20 100644 --- a/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts +++ b/yarn-project/end-to-end/src/spartan/mempool_limit.test.ts @@ -53,6 +53,9 @@ const TX_FLOOD_SIZE = 15; const TX_MEMPOOL_LIMIT = 10; const CONCURRENCY = 5; +// Tests mempool size limits on a live k8s deployment. Floods the mempool with more transactions than +// the configured limit, then verifies that the sequencer respects the cap and that excess transactions +// are either dropped or deferred without crashing the node. describe('mempool limiter test', () => { jest.setTimeout(10 * 60 * 2000); // 20 minutes let node: ReturnType; diff --git a/yarn-project/end-to-end/src/spartan/n_tps.test.ts b/yarn-project/end-to-end/src/spartan/n_tps.test.ts index 20e187efe53a..13d2cd7dabe0 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps.test.ts @@ -100,6 +100,9 @@ const peerCountQuery = () => `avg(aztec_peer_manager_peer_count_peers{k8s_namesp const peerConnectionDurationQuery = (perc: string, windowSeconds: number) => `histogram_quantile(${perc}, sum(rate(aztec_peer_manager_peer_connection_duration_milliseconds_bucket{k8s_namespace_name="${config.NAMESPACE}"}[${windowSeconds}s])) by (le))`; +// Sustained mixed-priority TPS test against a live k8s deployment. Drives LOW_VALUE_TPS and HIGH_VALUE_TPS +// traffic simultaneously, optionally with Chaos Mesh network shaping, and collects Prometheus metrics for +// p2p latency, attestation timing, and peer connections. describe('sustained N TPS test', () => { jest.setTimeout(60 * 60 * 1000 * 10); // 10 hours 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 3926725bac1f..7bc23728af59 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 @@ -109,6 +109,8 @@ type WalletTxProducer = { readyTx: Tx | null; }; +// End-to-end proving throughput test at TARGET_TPS against a live k8s deployment. Sends transactions, +// waits for the proven chain to advance by a full epoch, and collects Prometheus proving-queue metrics. describe(`prove ${TARGET_TPS}TPS test`, () => { // 4 hours: epoch boundary wait + tx sending (~40min) + tx mining + proving (~30min) jest.setTimeout(4 * 60 * 60 * 1000); diff --git a/yarn-project/end-to-end/src/spartan/prover-node.test.ts b/yarn-project/end-to-end/src/spartan/prover-node.test.ts index 299057536071..3b5db334ecfd 100644 --- a/yarn-project/end-to-end/src/spartan/prover-node.test.ts +++ b/yarn-project/end-to-end/src/spartan/prover-node.test.ts @@ -56,6 +56,9 @@ const enqueuedRootRollupJobs = { annotations: {}, }; +// Tests prover-node crash recovery against a live k8s deployment. Kills the prover broker and prover +// pods via kubectl, then watches Prometheus alert rules to confirm the node comes back online and resumes +// work from the cached proving-job state (BLOCK_ROOT_ROLLUP and ROOT_ROLLUP alerts fire as expected). describe('prover node recovery', () => { const endpoints: ServiceEndpoint[] = []; let runAlertCheck: ReturnType['runAlertCheck']; diff --git a/yarn-project/end-to-end/src/spartan/proving.test.ts b/yarn-project/end-to-end/src/spartan/proving.test.ts index 0c3bd5aa94fa..487110dc0894 100644 --- a/yarn-project/end-to-end/src/spartan/proving.test.ts +++ b/yarn-project/end-to-end/src/spartan/proving.test.ts @@ -12,6 +12,8 @@ const config = setupEnvironment(process.env); const logger = createLogger('e2e:spartan-test:proving'); const SLEEP_MS = 1000; +// Verifies that the proven chain tip advances on a live k8s deployment. Polls aztecNode.getBlockNumber('proven') +// until it surpasses the initial value, confirming at least one epoch was proven end-to-end. describe('proving test', () => { let aztecNode: AztecNode; const endpoints: ServiceEndpoint[] = []; diff --git a/yarn-project/end-to-end/src/spartan/reorg.test.ts b/yarn-project/end-to-end/src/spartan/reorg.test.ts index e01d4ee06f20..38ccc48c7d60 100644 --- a/yarn-project/end-to-end/src/spartan/reorg.test.ts +++ b/yarn-project/end-to-end/src/spartan/reorg.test.ts @@ -49,6 +49,9 @@ async function checkBalances(testAccounts: TestAccounts, mintAmount: bigint, tot ).toBe(totalAmountTransferred * BigInt(testAccounts.accounts.length)); } +// Reorg resilience test against a live k8s deployment. Runs transfers across multiple epochs, injects a +// prover failure via Chaos Mesh (CREATE_CHAOS_MESH), waits for chain recovery, and asserts token balances +// remain consistent after the reorg. describe('reorg test', () => { jest.setTimeout(210 * 60 * 1000); // 210 minutes diff --git a/yarn-project/end-to-end/src/spartan/reqresp_effectiveness.test.ts b/yarn-project/end-to-end/src/spartan/reqresp_effectiveness.test.ts index 435341a8b4af..e9bda6d4ece1 100644 --- a/yarn-project/end-to-end/src/spartan/reqresp_effectiveness.test.ts +++ b/yarn-project/end-to-end/src/spartan/reqresp_effectiveness.test.ts @@ -21,6 +21,9 @@ import { setupEnvironment, } from './utils.js'; +// Tests that req/resp gossip fallback works correctly when validators drop transactions. Configures a +// validator tx-drop rate via the admin API, then submits TARGET_TPS transactions and verifies inclusion, +// confirming that req/resp retrieval compensates for the lost gossip messages. describe('reqresp effectiveness under tx drop', () => { jest.setTimeout(60 * 60 * 1000); diff --git a/yarn-project/end-to-end/src/spartan/smoke.test.ts b/yarn-project/end-to-end/src/spartan/smoke.test.ts index 4a049ef8e1ea..d5a64dad6219 100644 --- a/yarn-project/end-to-end/src/spartan/smoke.test.ts +++ b/yarn-project/end-to-end/src/spartan/smoke.test.ts @@ -24,6 +24,9 @@ import { const config = setupEnvironment(process.env); +// Smoke checks for a live k8s deployment: node ENR reachable, committee forms within the validator-set lag +// window, first checkpoint mined, Chaos Mesh injectable, and all spartan port-forward paths open. +// Runs against the namespace set in NAMESPACE; port-forwards to RPC and Ethereum endpoints. describe('smoke test', () => { const logger = createLogger('e2e:spartan-test:smoke'); let aztecNode: AztecNode; diff --git a/yarn-project/end-to-end/src/spartan/transfer.test.ts b/yarn-project/end-to-end/src/spartan/transfer.test.ts index 7db4b2c0e2aa..2808eed26282 100644 --- a/yarn-project/end-to-end/src/spartan/transfer.test.ts +++ b/yarn-project/end-to-end/src/spartan/transfer.test.ts @@ -18,6 +18,8 @@ import { ChainHealth, type ServiceEndpoint, getRPCEndpoint, setupEnvironment } f const config = setupEnvironment(process.env); +// Basic token transfer test against a live k8s deployment. Deploys a sponsored FPC and token contract +// via the cluster's RPC endpoint, then executes private and public transfers between test accounts. describe('token transfer test', () => { jest.setTimeout(10 * 60 * 2000); // 20 minutes diff --git a/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts index 242db2945d6d..45272cd54cc0 100644 --- a/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/spartan/upgrade_governance_proposer.test.ts @@ -30,6 +30,9 @@ const config = setupEnvironment(process.env); const debugLogger = createLogger('e2e:spartan-test:upgrade_governance_proposer'); +// Governance proposer upgrade test against a live k8s deployment. Deploys a new GovernanceProposer +// payload on L1, drives the governance vote to completion, and asserts the rollup contract references +// the new proposer address. describe('spartan_upgrade_governance_proposer', () => { let aztecNode: AztecNode; let nodeInfo: NodeInfo; diff --git a/yarn-project/end-to-end/src/spartan/validator_ha.test.ts b/yarn-project/end-to-end/src/spartan/validator_ha.test.ts index beb6a48026b6..c791b4485f90 100644 --- a/yarn-project/end-to-end/src/spartan/validator_ha.test.ts +++ b/yarn-project/end-to-end/src/spartan/validator_ha.test.ts @@ -23,6 +23,9 @@ import { const logger = createLogger('e2e:spartan-test:validator-ha'); +// Validator high-availability test against a live k8s deployment. Kills KILL_PERCENT of sequencers via +// Chaos Mesh in repeated rounds, waits for the chain to recover to the next checkpoint each time, and +// asserts liveness is maintained throughout. describe('validator ha', () => { jest.setTimeout(60 * 60 * 1000); diff --git a/yarn-project/end-to-end/src/spartan/validator_nuke_and_suppression.test.ts b/yarn-project/end-to-end/src/spartan/validator_nuke_and_suppression.test.ts index b472febfbc1e..04aa2e27dc0f 100644 --- a/yarn-project/end-to-end/src/spartan/validator_nuke_and_suppression.test.ts +++ b/yarn-project/end-to-end/src/spartan/validator_nuke_and_suppression.test.ts @@ -27,6 +27,9 @@ import { waitForResourcesByName, } from './utils.js'; +// Tests validator network partition (suppression) and hard kill (nuke) with slashing on a live k8s +// deployment. Injects Chaos Mesh network-failure and pod-kill faults, then asserts that the surviving +// validators slash the offending validator and the chain continues to advance. describe('validator suppression and nuke with slashing assertions', () => { jest.setTimeout(2 * 60 * 60 * 1000); // 120 minutes diff --git a/yarn-project/p2p/package.json b/yarn-project/p2p/package.json index 4f1a4d537651..ff834999a31a 100644 --- a/yarn-project/p2p/package.json +++ b/yarn-project/p2p/package.json @@ -101,8 +101,7 @@ "semver": "^7.6.0", "sha3": "^2.1.4", "snappy": "^7.2.2", - "tslib": "^2.4.0", - "xxhash-wasm": "^1.1.0" + "tslib": "^2.4.0" }, "devDependencies": { "@aztec/archiver": "workspace:^", diff --git a/yarn-project/p2p/src/bootstrap/bootstrap.ts b/yarn-project/p2p/src/bootstrap/bootstrap.ts index 4f5ad00dd770..9053978d71b6 100644 --- a/yarn-project/p2p/src/bootstrap/bootstrap.ts +++ b/yarn-project/p2p/src/bootstrap/bootstrap.ts @@ -84,15 +84,21 @@ export class BootstrapNode implements P2PBootstrapApi { this.logger.info('Advertised socket address updated', { addr: addr.toString() }); }); this.node.on('discovered', async (enr: SignableENR) => { - const addr = await enr.getFullMultiaddr('udp'); - this.logger.verbose(`Discovered new peer`, { enr: enr.encodeTxt(), addr: addr?.toString() }); - // discv5's discovered() only updates routing table entries that already exist. Nodes that - // established a session with an empty-IP ENR are never inserted, so even after their ENR - // gains a valid socket address the routing table stays empty and FINDNODE always returns 0 - // peers. Calling addEnr() here does an insertOrUpdate regardless of prior state, fixing - // the routing table so these nodes become discoverable to other peers. - if (addr) { - this.node.addEnr(enr); + try { + const addr = await enr.getFullMultiaddr('udp'); + this.logger.verbose(`Discovered new peer`, { enr: enr.encodeTxt(), addr: addr?.toString() }); + // discv5's discovered() only updates routing table entries that already exist. Nodes that + // established a session with an empty-IP ENR are never inserted, so even after their ENR + // gains a valid socket address the routing table stays empty and FINDNODE always returns 0 + // peers. Calling addEnr() here does an insertOrUpdate regardless of prior state, fixing + // the routing table so these nodes become discoverable to other peers. + if (addr) { + this.node.addEnr(enr); + } + } catch (err) { + // A malformed ENR address field makes the parser throw. As an async listener this would crash + // the bootnode on an unhandled rejection — catch, log, and drop the ENR instead. + this.logger.warn(`Dropping discovered ENR with unparseable address`, { err }); } }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts index 3f3c4ae17d00..dab376208de9 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts @@ -26,6 +26,8 @@ import { type Tx, } from '@aztec/stdlib/tx'; +import { jest } from '@jest/globals'; + import { DataTxValidator } from './data_validator.js'; const mockTxs = (numTxs: number) => @@ -363,5 +365,42 @@ describe('TxDataValidator', () => { new RegExp(`${TX_ERROR_INCORRECT_CONTRACT_CLASS_ID}|${TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG}`), ); }); + + it('rejects an over-large declared bytecode length without allocating it', async () => { + const tx = await mockTx(5, { + numberOfNonRevertiblePublicCallRequests: 1, + numberOfRevertiblePublicCallRequests: 0, + }); + // A small log that declares a 16 MiB packed-bytecode length. The fixed-size class log can only + // carry ~93 KiB, so decoding it must reject rather than Buffer.alloc the attacker-declared size. + const overLargeByteLength = 16 * 1024 * 1024; + const headerFields = [ + new Fr(CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE), + Fr.random(), + new Fr(1), + Fr.random(), + Fr.random(), + new Fr(overLargeByteLength), + ]; + const allFields = [ + ...headerFields, + ...Array(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS - headerFields.length).fill(Fr.ZERO), + ]; + const fields = new ContractClassLogFields(allFields); + const log = new ContractClassLog(ProtocolContractAddress.ContractClassRegistry, fields, headerFields.length); + await injectContractClassLog(tx, log, headerFields.length); + await tx.recomputeHash(); + + const allocSpy = jest.spyOn(Buffer, 'alloc'); + try { + const result = await validator.validateTx(tx); + expect(result.result).toBe('invalid'); + expect(result.result === 'invalid' && result.reason[0]).toBe(TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG); + // The declared length was never allocated. + expect(allocSpy.mock.calls.some(([size]) => Number(size) >= overLargeByteLength)).toBe(false); + } finally { + allocSpy.mockRestore(); + } + }); }); }); diff --git a/yarn-project/p2p/src/services/discv5/discV5_service.ts b/yarn-project/p2p/src/services/discv5/discV5_service.ts index 5e1f3e0912d0..941d3429f5e2 100644 --- a/yarn-project/p2p/src/services/discv5/discV5_service.ts +++ b/yarn-project/p2p/src/services/discv5/discV5_service.ts @@ -1,5 +1,7 @@ import { createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; +import { DateProvider } from '@aztec/foundation/timer'; +import type { AztecAsyncKVStore } from '@aztec/kv-store'; import { type ComponentsVersions, checkCompressedComponentVersion } from '@aztec/stdlib/versioning'; import { OtelMetricsAdapter, type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -14,9 +16,13 @@ import { createNodeENR } from '../../enr/generate-enr.js'; import { AZTEC_ENR_KEY, Discv5Event, PeerEvent } from '../../types/index.js'; import { convertToMultiaddr } from '../../util.js'; import { type PeerDiscoveryService, PeerDiscoveryState } from '../service.js'; +import { PersistedEnrStore } from './persisted_enr_store.js'; const delayBeforeStart = 2000; // 2sec +/** Upper bound on persisted peer ENRs, to keep the store bounded as peers churn. */ +const MAX_PERSISTED_PEER_ENRS = 100; + /** * Peer discovery service using Discv5. */ @@ -35,14 +41,19 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService private bootstrapNodePeerIds: PeerId[] = []; public bootstrapNodeEnrs: ENR[] = []; private trustedPeerEnrs: ENR[] = []; + /** Node IDs of all config-provided peers (bootstrap, trusted, private); these are never persisted. */ + private readonly configProvidedNodeIds: Set; private startTime = 0; private currentIp: string | undefined; + /** Persistent store of discovered peer ENRs, used to re-seed discovery after a restart. */ + private readonly persistedEnrs?: PersistedEnrStore; + private handlers = { onMultiaddrUpdated: this.onMultiaddrUpdated.bind(this), - onDiscovered: this.onDiscovered.bind(this), + onPeerDiscovered: this.onPeerDiscovered.bind(this), onEnrAdded: this.onEnrAdded.bind(this), }; @@ -52,16 +63,27 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService private readonly packageVersion: string, telemetry: TelemetryClient = getTelemetryClient(), private logger = createLogger('p2p:discv5_service'), + store?: AztecAsyncKVStore, configOverrides: Partial = {}, + private readonly dateProvider: DateProvider = new DateProvider(), ) { super(); + this.persistedEnrs = store + ? new PersistedEnrStore(store, MAX_PERSISTED_PEER_ENRS, createLogger(`${this.logger.module}:enr-store`)) + : undefined; + const { p2pIp, p2pPort, p2pBroadcastPort, bootstrapNodes, trustedPeers, privatePeers } = config; this.currentIp = p2pIp; this.bootstrapNodeEnrs = bootstrapNodes.map(x => ENR.decodeTxt(x)); const privatePeerEnrs = new Set(privatePeers); this.trustedPeerEnrs = trustedPeers.filter(x => !privatePeerEnrs.has(x)).map(x => ENR.decodeTxt(x)); + // Peers supplied via config are re-injected from config on every start, so they must never end up + // in the persisted (discovered-peer) store — private peers especially must not be written there. + this.configProvidedNodeIds = new Set( + [...bootstrapNodes, ...trustedPeers, ...privatePeers].map(x => ENR.decodeTxt(x).nodeId), + ); // If no overridden broadcast port is provided, use the p2p port as the broadcast port if (!p2pBroadcastPort) { @@ -126,7 +148,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService } }; - this.discv5.on(Discv5Event.DISCOVERED, this.handlers.onDiscovered); + this.discv5.on(Discv5Event.DISCOVERED, this.handlers.onPeerDiscovered); this.discv5.on(Discv5Event.ENR_ADDED, this.handlers.onEnrAdded); this.discv5.on(Discv5Event.MULTIADDR_UPDATED, this.handlers.onMultiaddrUpdated); } @@ -168,7 +190,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService } this.logger.debug('Starting DiscV5'); await this.discv5.start(); - this.startTime = Date.now(); + this.startTime = this.dateProvider.now(); const enrUpdateEnabled = this.config.queryForIp || !this.config.p2pIp; this.logger.info(`DiscV5 service started`, { @@ -220,6 +242,26 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService this.discv5.addEnr(enr); } } + + // Re-seed discovery from peers persisted on previous runs. This lets a node rejoin the network + // after a restart even when no bootstrap node is reachable: discv5 has no on-disk routing table, + // so without this its DHT would start empty and rediscovery would depend on inbound dials alone. + if (this.persistedEnrs) { + // Drop peers that are now config-provided (re-injected above) or fail version checks. + const enrs = await this.persistedEnrs.load( + enr => this.validateEnr(enr) && !this.configProvidedNodeIds.has(enr.nodeId), + ); + for (const enr of enrs) { + try { + this.discv5.addEnr(enr); + } catch (err) { + this.logger.debug(`Error re-seeding persisted ENR ${enr.nodeId}`, err); + } + } + if (enrs.length > 0) { + this.logger.info(`Re-seeded discovery with ${enrs.length} persisted peer ENRs`); + } + } } public async runRandomNodesQuery(): Promise { @@ -229,8 +271,8 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService // First, wait some time before starting the peer discovery // reference: https://github.com/ChainSafe/lodestar/issues/3423 - const msSinceStart = Date.now() - this.startTime; - if (Date.now() - this.startTime <= delayBeforeStart) { + const msSinceStart = this.dateProvider.now() - this.startTime; + if (this.dateProvider.now() - this.startTime <= delayBeforeStart) { await sleep(delayBeforeStart - msSinceStart); } @@ -265,7 +307,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService if (this.currentState !== PeerDiscoveryState.RUNNING) { return; } - await this.discv5.off(Discv5Event.DISCOVERED, this.handlers.onDiscovered); + await this.discv5.off(Discv5Event.DISCOVERED, this.handlers.onPeerDiscovered); await this.discv5.off(Discv5Event.ENR_ADDED, this.handlers.onEnrAdded); await this.discv5.off(Discv5Event.MULTIADDR_UPDATED, this.handlers.onMultiaddrUpdated); @@ -275,12 +317,38 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService } private async onEnrAdded(enr: ENR) { - const multiAddrTcp = await enr.getFullMultiaddr('tcp'); - const multiAddrUdp = await enr.getFullMultiaddr('udp'); - this.logger.debug(`Added ENR ${enr.encodeTxt()}`, { multiAddrTcp, multiAddrUdp, nodeId: enr.nodeId }); + try { + const multiAddrTcp = await enr.getFullMultiaddr('tcp'); + const multiAddrUdp = await enr.getFullMultiaddr('udp'); + this.logger.debug(`Added ENR ${enr.encodeTxt()}`, { multiAddrTcp, multiAddrUdp, nodeId: enr.nodeId }); + // A peer just entered the routing table — persist it so we can re-seed discovery after a restart. + if (this.persistedEnrs && this.shouldPersist(enr)) { + await this.persistedEnrs.persist(enr); + } + this.onDiscovered(enr); + } catch (err) { + // A malformed ENR address field (e.g. a 3-byte TCP port) makes @nethermindeth/enr throw while + // parsing the multiaddr. This is an async event listener, so an unhandled rejection would crash + // the process — catch, log, and drop the ENR instead. + this.logger.warn(`Dropping ENR with unparseable address`, { nodeId: enr.nodeId, err }); + } + } + + private async onPeerDiscovered(enr: ENR) { + // discv5 updates an existing routing-table entry on an ENR sequence bump without emitting enrAdded, + // so DISCOVERED is our only signal that a peer we already track may have changed address. Refresh + // the persisted copy (a no-op for peers we don't already store) so we don't keep dialing a stale ENR. + if (this.persistedEnrs && this.shouldPersist(enr)) { + await this.persistedEnrs.refresh(enr); + } this.onDiscovered(enr); } + /** Whether an ENR belongs in the persisted store: a validated, non-config peer that isn't ourselves. */ + private shouldPersist(enr: ENR): boolean { + return !this.configProvidedNodeIds.has(enr.nodeId) && enr.nodeId !== this.enr.nodeId && this.validateEnr(enr); + } + private isOurBootnode(enr: ENR) { return this.bootstrapNodeEnrs.some(x => x.nodeId === enr.nodeId); } @@ -314,6 +382,17 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService return false; } + // A malformed TCP field (e.g. a non-2-byte port) makes @nethermindeth/enr throw while building the + // multiaddr. Reject such an ENR here so it's never emitted as a full peer or re-parsed by an + // unguarded listener (which would crash the process). A missing TCP addr is allowed — those peers + // are simply skipped at dial time. + try { + enr.getLocationMultiaddr('tcp'); + } catch (err) { + this.logger.debug(`Peer node ${enr.nodeId} has an unparseable TCP address`, { err }); + return false; + } + // And check it has the correct version let compressedVersion; try { diff --git a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts index 8dc4d876883d..e5eb85c2f914 100644 --- a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts +++ b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts @@ -8,13 +8,16 @@ import { getTelemetryClient } from '@aztec/telemetry-client'; import { jest } from '@jest/globals'; import type { PeerId } from '@libp2p/interface'; import { createSecp256k1PeerId } from '@libp2p/peer-id-factory'; +import { multiaddr } from '@multiformats/multiaddr'; import type { IDiscv5CreateOptions } from '@nethermindeth/discv5'; +import { ENR, SignableENR } from '@nethermindeth/enr'; import { BootstrapNode } from '../../bootstrap/bootstrap.js'; import { type BootnodeConfig, DEFAULT_PUBLIC_IP_SERVICES, type P2PConfig, getP2PDefaultConfig } from '../../config.js'; -import { AZTEC_ENR_CLIENT_VERSION_KEY } from '../../types/index.js'; +import { AZTEC_ENR_CLIENT_VERSION_KEY, AZTEC_ENR_KEY, PeerEvent } from '../../types/index.js'; import { PeerDiscoveryState } from '../service.js'; import { DiscV5Service } from './discV5_service.js'; +import { PersistedEnrStore } from './persisted_enr_store.js'; /** * Runs discovery queries on all nodes until the condition is met or timeout expires. @@ -221,30 +224,73 @@ describe('Discv5Service', () => { await stopNodes(node1, node2, node3); }); - // Test is flakey, so skipping for now. - // TODO: Investigate: #6246 - it.skip('should persist peers without bootnode', async () => { - const node1 = await createNode(); - const node2 = await createNode(); - await node1.start(); - await node2.start(); + it('resurrects persisted peers from the store on restart with no bootnode', async () => { + const peerIdA = await createSecp256k1PeerId(); + const portA = ++basePort; + + // nodeA is backed by the suite's store (the only KV store in play — the bootnode shares it), so it + // persists the peers it discovers. We can recreate it as a fresh instance with the same identity, + // port and store to model a process restart. nodeB has no store; it's only a peer for nodeA to find. + const makeNodeA = (useBootnode: boolean) => + new DiscV5Service( + peerIdA, + { + ...getP2PDefaultConfig(), + ...emptyChainConfig, + p2pIp: `127.0.0.1`, + listenAddress: `127.0.0.1`, + p2pPort: portA, + bootstrapNodes: useBootnode ? [bootNode.getENR().encodeTxt()] : [], + blockCheckIntervalMS: 50, + peerCheckIntervalMS: 50, + p2pEnabled: true, + l2QueueSize: 100, + }, + testPackageVersion, + undefined, + undefined, + store, + ); + + let nodeA = makeNodeA(true); + const nodeB = await createNode(); + await nodeA.start(); + await nodeB.start(); + + // A read-only view of the same store, to assert what nodeA persists. + const persistedView = new PersistedEnrStore(store, 1000); + const persistedNodeIds = async () => (await persistedView.load(() => true)).map(enr => enr.nodeId); + + // Drive discovery until nodeA has found nodeB and persisted its ENR — exercising the persist hook + // for a discovered, non-bootnode peer. + await retryUntil( + async () => { + await Promise.all([nodeA, nodeB].map(n => n.runRandomNodesQuery())); + return (await persistedNodeIds()).includes(nodeB.getEnr().nodeId) || undefined; + }, + 'nodeB persisted by nodeA', + 30, + 0.2, + ); - await runDiscoveryUntil([node1, node2], () => node2.getKadValues().length >= 2); + // The bootnode is config-provided, so it must never be persisted. + expect(await persistedNodeIds()).not.toContain(bootNode.getENR().nodeId); - await node2.stop(); + // Tear the live network down so nothing can re-teach nodeA about nodeB. + await nodeA.stop(); + await nodeB.stop(); await bootNode.stop(); - await node2.start(); + // Recreate nodeA as a fresh instance on the same store with no bootnode. Its discv5 routing table + // starts empty, so the only way it can know nodeB is by resurrecting the persisted ENR on start(). + nodeA = makeNodeA(false); + await nodeA.start(); - await runDiscoveryUntil([node1, node2], () => node2.getKadValues().length >= 1); + const resurrected = await Promise.all(nodeA.getKadValues().map(async enr => (await enr.peerId()).toString())); + expect(resurrected).toContain(nodeB.getPeerId().toString()); + expect(resurrected).not.toContain(bootNodePeerId.toString()); - const node2Peers = await Promise.all(node2.getKadValues().map(async peer => (await peer.peerId()).toString())); - // NOTE: bootnode seems to still be present in list of peers sometimes, will investigate - // expect(node2Peers).toHaveLength(1); - expect(node2Peers).toContain(node1.getPeerId().toString()); - - await node1.stop(); - await node2.stop(); + await nodeA.stop(); }); it('should use trusted peers for discovery', async () => { @@ -347,6 +393,45 @@ describe('Discv5Service', () => { expect(enr.kvs.get(AZTEC_ENR_CLIENT_VERSION_KEY)?.toString()).toEqual(testPackageVersion); }); + describe('malformed ENR address handling', () => { + // Builds an ENR with valid ip/udp and a (correct-version) aztec key, but a malformed 3-byte TCP + // value that makes @nethermindeth/enr throw while parsing the multiaddr. Returns the malformed ENR + // and a valid control built from the same identity (valid version, no malformed TCP). + const makeMalformedTcpEnr = async (aztecKey: Buffer) => { + const peerId = await createSecp256k1PeerId(); + const signable = SignableENR.createFromPeerId(peerId); + signable.setLocationMultiaddr(multiaddr('/ip4/127.0.0.1/udp/9999')); + signable.set(AZTEC_ENR_KEY, aztecKey); + const control = ENR.decodeTxt(signable.encodeTxt()); + signable.set('tcp', Buffer.from([0x00, 0x00, 0x00])); + const malformed = ENR.decodeTxt(signable.encodeTxt()); + return { control, malformed }; + }; + + it('validateEnr rejects an ENR whose TCP address will not parse', async () => { + const service = await createNode(); + const aztecKey = Buffer.from(service.getEnr().kvs.get(AZTEC_ENR_KEY)!); + const { control, malformed } = await makeMalformedTcpEnr(aztecKey); + + // The control (same valid version, no malformed TCP) validates; only the malformed TCP is rejected. + expect((service as any).validateEnr(control)).toBe(true); + expect((service as any).validateEnr(malformed)).toBe(false); + }); + + it('onEnrAdded does not reject or emit DISCOVERED for a malformed-TCP ENR', async () => { + const service = await createNode(); + const aztecKey = Buffer.from(service.getEnr().kvs.get(AZTEC_ENR_KEY)!); + const { malformed } = await makeMalformedTcpEnr(aztecKey); + + const discovered: ENR[] = []; + service.on(PeerEvent.DISCOVERED, (enr: ENR) => discovered.push(enr)); + + // Before the fix this rejected with a RangeError, crashing the process as an unhandled rejection. + await expect((service as any).onEnrAdded(malformed)).resolves.toBeUndefined(); + expect(discovered).toEqual([]); + }); + }); + const testPackageVersion = 'test-discv5-service'; const createNode = async (overrides: Partial = {}, useBootnode = true) => { const port = ++basePort; @@ -365,6 +450,6 @@ describe('Discv5Service', () => { l2QueueSize: 100, ...overrides, }; - return new DiscV5Service(peerId, config, testPackageVersion, undefined, undefined, overrides); + return new DiscV5Service(peerId, config, testPackageVersion, undefined, undefined, undefined, overrides); }; }); diff --git a/yarn-project/p2p/src/services/discv5/persisted_enr_store.test.ts b/yarn-project/p2p/src/services/discv5/persisted_enr_store.test.ts new file mode 100644 index 000000000000..a55ef6870ba5 --- /dev/null +++ b/yarn-project/p2p/src/services/discv5/persisted_enr_store.test.ts @@ -0,0 +1,136 @@ +import type { AztecAsyncKVStore } from '@aztec/kv-store'; +import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; + +import type { PeerId } from '@libp2p/interface'; +import { createSecp256k1PeerId } from '@libp2p/peer-id-factory'; +import { multiaddr } from '@multiformats/multiaddr'; +import { ENR, SignableENR } from '@nethermindeth/enr'; + +import { PersistedEnrStore } from './persisted_enr_store.js'; + +describe('PersistedEnrStore', () => { + let store: AztecAsyncKVStore; + + beforeEach(async () => { + store = await openTmpStore('persisted-enr-store-test'); + }); + + afterEach(async () => { + await store.close(); + }); + + // Mints an immutable ENR for the given peer at a specific sequence number and UDP port. The same + // peer always produces the same nodeId regardless of sequence/address, so this models a peer that + // bumps its ENR (e.g. after changing address). + const enrForPeer = (peerId: PeerId, seq: bigint, udpPort: number): ENR => { + const signable = SignableENR.createFromPeerId(peerId); + signable.setLocationMultiaddr(multiaddr(`/ip4/127.0.0.1/udp/${udpPort}`)); + signable.seq = seq; + return ENR.decodeTxt(signable.encodeTxt()); + }; + + // Loads the store and indexes the result by nodeId so tests can assert on individual peers' ENRs. + const loadByNodeId = async (enrStore: PersistedEnrStore, accept: (enr: ENR) => boolean = () => true) => { + const enrs = await enrStore.load(accept); + return new Map(enrs.map(enr => [enr.nodeId, enr])); + }; + + // Asserts the store holds exactly `expected`, matching each by full ENR record (txt), sequence and port. + const expectStoredExactly = async (enrStore: PersistedEnrStore, expected: ENR[]) => { + const loaded = await loadByNodeId(enrStore); + expect([...loaded.keys()].sort()).toEqual(expected.map(e => e.nodeId).sort()); + for (const enr of expected) { + const stored = loaded.get(enr.nodeId); + expect(stored?.encodeTxt()).toBe(enr.encodeTxt()); + expect(stored?.seq).toBe(enr.seq); + expect(stored?.udp).toBe(enr.udp); + } + }; + + it('round-trips persisted ENRs through load, preserving their full record', async () => { + const enrStore = new PersistedEnrStore(store, 10); + const a = enrForPeer(await createSecp256k1PeerId(), 1n, 4001); + const b = enrForPeer(await createSecp256k1PeerId(), 1n, 4002); + + await enrStore.persist(a); + await enrStore.persist(b); + + await expectStoredExactly(enrStore, [a, b]); + }); + + it('refreshes a tracked peer to a newer ENR (new address), ignores older ones, never adds unknown peers', async () => { + const enrStore = new PersistedEnrStore(store, 10); + const peer = await createSecp256k1PeerId(); + const v1 = enrForPeer(peer, 1n, 5001); + const v2 = enrForPeer(peer, 2n, 5002); + const v3 = enrForPeer(peer, 3n, 5003); + const nodeId = v1.nodeId; // v1/v2/v3 share a nodeId (same peer), so any of them works + + await enrStore.persist(v1); + await expectStoredExactly(enrStore, [v1]); + + // A newer ENR (higher sequence, new address) replaces the stored one in full. + await enrStore.refresh(v3); + let stored = (await loadByNodeId(enrStore)).get(nodeId); + expect(stored?.encodeTxt()).toBe(v3.encodeTxt()); + expect(stored?.seq).toBe(3n); + expect(stored?.udp).toBe(5003); + + // An older ENR is ignored — the stored record and address are unchanged. + await enrStore.refresh(v2); + stored = (await loadByNodeId(enrStore)).get(nodeId); + expect(stored?.encodeTxt()).toBe(v3.encodeTxt()); + expect(stored?.seq).toBe(3n); + expect(stored?.udp).toBe(5003); + + // Refreshing a peer we don't already track is a no-op (refresh only updates, never inserts). + const unknown = enrForPeer(await createSecp256k1PeerId(), 1n, 5099); + await enrStore.refresh(unknown); + const loaded = await loadByNodeId(enrStore); + expect(loaded.has(unknown.nodeId)).toBe(false); + await expectStoredExactly(enrStore, [v3]); + }); + + it('evicts the oldest-seen ENRs first once over capacity, keeping the rest intact', async () => { + const enrStore = new PersistedEnrStore(store, 3); + const a = enrForPeer(await createSecp256k1PeerId(), 1n, 6001); + const b = enrForPeer(await createSecp256k1PeerId(), 1n, 6002); + const c = enrForPeer(await createSecp256k1PeerId(), 1n, 6003); + const d = enrForPeer(await createSecp256k1PeerId(), 1n, 6004); + const e = enrForPeer(await createSecp256k1PeerId(), 1n, 6005); + + // Fill past the cap: A is the oldest-seen and gets evicted; B, C, D remain with their records intact. + await enrStore.persist(a); + await enrStore.persist(b); + await enrStore.persist(c); + await enrStore.persist(d); + let loaded = await loadByNodeId(enrStore); + expect(loaded.has(a.nodeId)).toBe(false); + await expectStoredExactly(enrStore, [b, c, d]); + + // Re-persisting B marks it most-recently-seen, so the next eviction drops C (now the oldest), not B. + await enrStore.persist(b); + await enrStore.persist(e); + loaded = await loadByNodeId(enrStore); + expect(loaded.has(a.nodeId)).toBe(false); + expect(loaded.has(c.nodeId)).toBe(false); + await expectStoredExactly(enrStore, [b, d, e]); + }); + + it('drops rejected and undecodable entries on load, keeping the accepted record intact', async () => { + const enrStore = new PersistedEnrStore(store, 10); + const keep = enrForPeer(await createSecp256k1PeerId(), 1n, 7001); + const reject = enrForPeer(await createSecp256k1PeerId(), 1n, 7002); + + await enrStore.persist(keep); + await enrStore.persist(reject); + + // The predicate rejects one peer; it is excluded from the result and deleted from the store. + const loaded = await loadByNodeId(enrStore, enr => enr.nodeId === keep.nodeId); + expect([...loaded.keys()]).toEqual([keep.nodeId]); + expect(loaded.get(keep.nodeId)?.encodeTxt()).toBe(keep.encodeTxt()); + + // A subsequent accept-all load confirms the rejected entry was deleted, not merely filtered out. + await expectStoredExactly(enrStore, [keep]); + }); +}); diff --git a/yarn-project/p2p/src/services/discv5/persisted_enr_store.ts b/yarn-project/p2p/src/services/discv5/persisted_enr_store.ts new file mode 100644 index 000000000000..3e6ec92625fd --- /dev/null +++ b/yarn-project/p2p/src/services/discv5/persisted_enr_store.ts @@ -0,0 +1,158 @@ +import { type Logger, createLogger } from '@aztec/foundation/log'; +import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; + +import { ENR } from '@nethermindeth/enr'; + +/** Map name under which discovered peer ENRs are persisted so discovery can be re-seeded after a restart. */ +const PERSISTED_ENRS_MAP_NAME = 'discovered_peer_enrs'; + +/** + * A persisted peer entry: the ENR text plus a monotonic sequence number recording when the peer was + * last seen. The sequence orders the store as a FIFO so eviction drops the oldest-seen peers first. + */ +interface PersistedEnr { + enr: string; + seq: number; +} + +function parsePersistedEnr(value: string): PersistedEnr | undefined { + try { + const parsed = JSON.parse(value); + if (typeof parsed?.enr === 'string' && typeof parsed?.seq === 'number') { + return parsed; + } + } catch { + // Unparseable entry; caller treats it as stale. + } + return undefined; +} + +/** + * A bounded, transactional store of discovered peer ENRs used to re-seed discovery after a restart. + * + * Entries are keyed by nodeId (so re-seeing a peer dedups in place) and carry a monotonic sequence so + * the store behaves as a FIFO: when it exceeds its cap, the oldest-seen peers are evicted first. All + * writes run in a transaction so concurrent discovery events can't race the size check. + */ +export class PersistedEnrStore { + private readonly enrs: AztecAsyncMap; + /** Monotonic sequence stamped on each entry; seeded from the store's max in {@link load}. */ + private seq = 0; + + constructor( + private readonly store: AztecAsyncKVStore, + private readonly maxEntries: number, + private readonly logger: Logger = createLogger('p2p:discv5:enr-store'), + ) { + this.enrs = store.openMap(PERSISTED_ENRS_MAP_NAME); + } + + /** Adds or replaces a peer's ENR, stamping it as most-recently-seen and evicting the oldest if over cap. */ + public async persist(enr: ENR): Promise { + const nodeId = enr.nodeId; + const enrTxt = enr.encodeTxt(); + try { + await this.store.transactionAsync(async () => { + const seq = ++this.seq; + const others: { nodeId: string; seq: number }[] = []; + for await (const [id, value] of this.enrs.entriesAsync()) { + if (id !== nodeId) { + others.push({ nodeId: id, seq: parsePersistedEnr(value)?.seq ?? 0 }); + } + } + const overflow = others.length + 1 - this.maxEntries; + if (overflow > 0) { + others.sort((a, b) => a.seq - b.seq); // oldest-seen (lowest sequence) first + for (let i = 0; i < overflow; i++) { + await this.enrs.delete(others[i].nodeId); + } + } + await this.enrs.set(nodeId, JSON.stringify({ enr: enrTxt, seq })); + }); + } catch (err) { + this.logger.warn(`Failed to persist discovered ENR ${nodeId}`, err); + } + } + + /** + * Refreshes an already-persisted peer's stored ENR when a newer one (higher ENR sequence) is seen. + * A no-op for peers we don't already track — we never add new peers via this path, only update. + * + * This exists because discv5 updates an existing routing-table entry on an ENR sequence bump without + * emitting an `enrAdded` event, so a peer that changes address would otherwise keep its stale ENR + * persisted until evicted. + */ + public async refresh(enr: ENR): Promise { + const nodeId = enr.nodeId; + try { + await this.store.transactionAsync(async () => { + const existing = await this.enrs.getAsync(nodeId); + if (!existing) { + return; + } + // Two unrelated sequence numbers are in play: + // - the ENR sequence (`enr.seq` / `storedEnrSeq`) is set by the *peer* and bumped whenever it + // revises its record (e.g. changes address); we compare these to decide whether the incoming + // ENR is genuinely newer and worth keeping. + // - our storage sequence (`this.seq`) is an internal counter used only for FIFO eviction; we + // stamp a fresh one so this entry counts as most-recently-seen. + const storedEnrSeq = this.decodeStoredEnr(existing)?.seq; + if (storedEnrSeq !== undefined && enr.seq <= storedEnrSeq) { + return; + } + await this.enrs.set(nodeId, JSON.stringify({ enr: enr.encodeTxt(), seq: ++this.seq })); + }); + } catch (err) { + this.logger.warn(`Failed to refresh persisted ENR ${nodeId}`, err); + } + } + + /** + * Loads all persisted ENRs, decoding each and keeping those the `accept` predicate allows. Entries + * that fail to decode or are rejected are removed from the store. The FIFO sequence resumes from the + * highest persisted value so eviction order survives restarts. + */ + public async load(accept: (enr: ENR) => boolean): Promise { + const valid: ENR[] = []; + const stale: string[] = []; + let maxSeq = 0; + for await (const [nodeId, value] of this.enrs.entriesAsync()) { + const parsed = parsePersistedEnr(value); + let enr: ENR | undefined; + if (parsed) { + try { + enr = ENR.decodeTxt(parsed.enr); + } catch (err) { + this.logger.debug(`Dropping undecodable persisted ENR ${nodeId}`, err); + } + } + if (!parsed || !enr || !accept(enr)) { + stale.push(nodeId); + continue; + } + maxSeq = Math.max(maxSeq, parsed.seq); + valid.push(enr); + } + this.seq = maxSeq; + if (stale.length > 0) { + await this.store.transactionAsync(async () => { + for (const nodeId of stale) { + await this.enrs.delete(nodeId); + } + }); + } + return valid; + } + + private decodeStoredEnr(value: string): ENR | undefined { + const parsed = parsePersistedEnr(value); + if (!parsed) { + return undefined; + } + try { + return ENR.decodeTxt(parsed.enr); + } catch { + return undefined; + } + } +} diff --git a/yarn-project/p2p/src/services/encoding.test.ts b/yarn-project/p2p/src/services/encoding.test.ts index 2be4857d29ce..a78f21aae2a2 100644 --- a/yarn-project/p2p/src/services/encoding.test.ts +++ b/yarn-project/p2p/src/services/encoding.test.ts @@ -1,8 +1,36 @@ -import { MAX_TX_SIZE_KB, TopicType } from '@aztec/stdlib/p2p'; +import { MAX_TX_SIZE_KB, P2PMessage, TopicType } from '@aztec/stdlib/p2p'; import { compressSync, uncompressSync } from 'snappy'; -import { SnappyTransform, readSnappyPreamble } from './encoding.js'; +import { SnappyTransform, getMsgIdFn, readSnappyPreamble } from './encoding.js'; + +describe('getMsgIdFn', () => { + it('frames the topic length so a boundary-shifted (topic, data) pair does not collide', async () => { + const topic = '/aztec/tx/0.1.0'; + const data = new P2PMessage(Buffer.from('deadbeefcafe', 'hex')).toMessageData(); + + // Aztec gossip data starts with a 4-byte BE length prefix, so data[0] === 0x00. + expect(data[0]).toBe(0x00); + + // Shift one byte across the topic/data boundary: T' = T + data[0], D' = data[1:]. Under a raw + // topic||data concatenation these hash identically to (T, data) — the suppression attack. + const shiftedTopic = topic + String.fromCharCode(data[0]); + const shiftedData = data.subarray(1); + + const id = await getMsgIdFn({ topic, data }); + const shiftedId = await getMsgIdFn({ topic: shiftedTopic, data: shiftedData }); + + expect(Buffer.from(shiftedId).equals(Buffer.from(id))).toBe(false); + }); + + it('is deterministic for the same (topic, data)', async () => { + const topic = '/aztec/tx/0.1.0'; + const data = new P2PMessage(Buffer.from('0102030405', 'hex')).toMessageData(); + const a = await getMsgIdFn({ topic, data }); + const b = await getMsgIdFn({ topic, data }); + expect(Buffer.from(a).equals(Buffer.from(b))).toBe(true); + }); +}); describe('readSnappyPreamble', () => { describe('basic varint decoding', () => { diff --git a/yarn-project/p2p/src/services/encoding.ts b/yarn-project/p2p/src/services/encoding.ts index d21fbe695597..5ecf5f2168f1 100644 --- a/yarn-project/p2p/src/services/encoding.ts +++ b/yarn-project/p2p/src/services/encoding.ts @@ -2,12 +2,10 @@ import { createLogger } from '@aztec/foundation/log'; import { MAX_TX_SIZE_KB, TopicType, getTopicFromString } from '@aztec/stdlib/p2p'; -import type { RPC } from '@chainsafe/libp2p-gossipsub/message'; import type { DataTransform } from '@chainsafe/libp2p-gossipsub/types'; import type { Message } from '@libp2p/interface'; import { webcrypto } from 'node:crypto'; import { compressSync, uncompressSync } from 'snappy'; -import xxhashFactory from 'xxhash-wasm'; /** Thrown when a Snappy-compressed response exceeds the allowed decompressed size. */ export class OversizedSnappyResponseError extends Error { @@ -17,26 +15,9 @@ export class OversizedSnappyResponseError extends Error { } } -// Load WASM -const xxhash = await xxhashFactory(); - -// Use salt to prevent msgId from being mined for collisions -const h64Seed = BigInt(Math.floor(Math.random() * 1e9)); - // Shared buffer to convert msgId to string const sharedMsgIdBuf = Buffer.alloc(20); -/** - * The function used to generate a gossipsub message id - * We use the first 8 bytes of SHA256(data) for content addressing - */ -export function fastMsgIdFn(rpcMsg: RPC.Message): string { - if (rpcMsg.data) { - return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16); - } - return '0000000000000000'; -} - export function msgIdToStrFn(msgId: Uint8Array): string { // This happens serially, no need to reallocate the buffer sharedMsgIdBuf.set(msgId); @@ -49,11 +30,20 @@ export function msgIdToStrFn(msgId: Uint8Array): string { * Follows similarly to: * https://github.com/ethereum/consensus-specs/blob/v1.1.0-alpha.7/specs/altair/p2p-interface.md#topics-and-messages * + * The topic length is framed into the hash input (`uint32be(topicLen) || topic || data`) so the + * `(topic, data)` boundary is unambiguous. A raw `topic || data` concatenation is not injective — + * shifting bytes across the boundary (e.g. `(topic + data[0], data[1:])`) yields the same bytes and + * thus the same id, letting an unsubscribed-topic message pre-occupy a real message's seenCache slot + * and suppress it as a duplicate. + * * @param message - The libp2p message * @returns The message identifier */ -export async function getMsgIdFn({ topic, data }: Message): Promise { - const buffer = Buffer.concat([Buffer.from(topic), data]); +export async function getMsgIdFn({ topic, data }: Pick): Promise { + const topicBytes = Buffer.from(topic); + const framedTopicLength = Buffer.allocUnsafe(4); + framedTopicLength.writeUInt32BE(topicBytes.length); + const buffer = Buffer.concat([framedTopicLength, topicBytes, data]); const hash = await webcrypto.subtle.digest('SHA-256', buffer); return Buffer.from(hash.slice(0, 20)); } diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 63dccf33be04..1c573b26b58b 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -1,6 +1,6 @@ import type { EpochCacheInterface } from '@aztec/epoch-cache'; import { BlockNumber, type SlotNumber } from '@aztec/foundation/branded-types'; -import { maxBy, merge } from '@aztec/foundation/collection'; +import { compactArray, maxBy, merge } from '@aztec/foundation/collection'; import { type Logger, createLibp2pComponentLogger, createLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; import { Timer } from '@aztec/foundation/timer'; @@ -83,7 +83,7 @@ import { type PubSubLibp2p, convertToMultiaddr } from '../../util.js'; import { getVersions } from '../../versioning.js'; import { AztecDatastore } from '../data_store.js'; import { DiscV5Service } from '../discv5/discV5_service.js'; -import { SnappyTransform, fastMsgIdFn, getMsgIdFn, msgIdToStrFn } from '../encoding.js'; +import { SnappyTransform, getMsgIdFn, msgIdToStrFn } from '../encoding.js'; import { APP_SPECIFIC_WEIGHT, gossipScoreThresholds } from '../gossipsub/scoring.js'; import { createAllTopicScoreParams } from '../gossipsub/topic_score_params.js'; import type { PeerManagerInterface } from '../peer-manager/interface.js'; @@ -368,6 +368,7 @@ export class LibP2PService extends WithTracer implements P2PService { packageVersion, telemetry, createLogger(`${logger.module}:discv5_service`, logger.getBindings()), + peerStore, ); // Seed libp2p's bootstrap discovery with private and trusted peers @@ -382,21 +383,27 @@ export class LibP2PService extends WithTracer implements P2PService { const protocolVersion = compressComponentVersions(versions); const preferredPeersEnrs: ENR[] = config.preferredPeers.map(enr => ENR.decodeTxt(enr)); - const directPeers = ( + const directPeers = compactArray( await Promise.all( preferredPeersEnrs.map(async enr => { - const peerId = await enr.peerId(); - const address = enr.getLocationMultiaddr('tcp'); - if (address === undefined) { - throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`); + try { + const peerId = await enr.peerId(); + const address = enr.getLocationMultiaddr('tcp'); + if (address === undefined) { + throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`); + } + return { + id: peerId, + addrs: [address], + }; + } catch (err) { + // A malformed configured ENR shouldn't abort node setup — skip it and log. + logger.warn(`Skipping preferred peer with invalid ENR`, { err }); + return undefined; } - return { - id: peerId, - addrs: [address], - }; }), - ) - ).filter(peer => peer !== undefined); + ), + ); const announceTcpMultiaddr = config.p2pIp ? [convertToMultiaddr(config.p2pIp, p2pPort, 'tcp')] : []; @@ -412,6 +419,13 @@ export class LibP2PService extends WithTracer implements P2PService { expectedBlockProposalsPerSlot: config.expectedBlockProposalsPerSlot, }); + // Restrict gossipsub to exactly the topics we subscribe to. Without this, an arbitrary-topic + // message is transformed, msg-id'd and inserted into the seenCache before the subscription check, + // so a crafted topic colliding on msg id can suppress a real message as a duplicate. + const allowedTopics = getTopicsForConfig(config.disableTransactions).map(topic => + createTopicString(topic, protocolVersion), + ); + const node = await createLibp2p({ start: false, peerId, @@ -496,9 +510,13 @@ export class LibP2PService extends WithTracer implements P2PService { mcacheLength: config.gossipsubMcacheLength, mcacheGossip: config.gossipsubMcacheGossip, seenTTL: config.gossipsubSeenTTL, + allowedTopics, + // No fastMsgIdFn: the fast-path dedup cache keys on a non-cryptographic 64-bit hash of the + // raw data only (no topic), so a collision — accidental or engineered via a weak seed — drops + // a different message with no fallback to the full id. Dedup instead rests solely on the + // cryptographic, topic-framed msgIdFn below. msgIdFn: getMsgIdFn, msgIdToStrFn: msgIdToStrFn, - fastMsgIdFn: fastMsgIdFn, dataTransform: new SnappyTransform(), metricsRegister: otelMetricsAdapter, metricsTopicStrToLabel: metricsTopicStrToLabels(protocolVersion), diff --git a/yarn-project/p2p/src/services/peer-manager/peer_manager.ts b/yarn-project/p2p/src/services/peer-manager/peer_manager.ts index a7ba6f37e0ad..809ed1def3ee 100644 --- a/yarn-project/p2p/src/services/peer-manager/peer_manager.ts +++ b/yarn-project/p2p/src/services/peer-manager/peer_manager.ts @@ -1,4 +1,5 @@ import type { EpochCacheInterface } from '@aztec/epoch-cache'; +import { compactArray } from '@aztec/foundation/collection'; import { makeEthSignDigest, tryRecoverAddress } from '@aztec/foundation/crypto/secp256k1-signer'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { EthAddress } from '@aztec/foundation/eth-address'; @@ -199,21 +200,27 @@ export class PeerManager implements PeerManagerInterface { .then(peerIds => peerIds.forEach(peerId => this.preferredPeers.add(peerId.toString()))) .catch(e => this.logger.error('Error initializing preferred peers', e)); - const directPeers = ( + const directPeers = compactArray( await Promise.all( preferredPeersEnrs.map(async enr => { - const peerId = await enr.peerId(); - const address = enr.getLocationMultiaddr('tcp'); - if (address === undefined) { - throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`); + try { + const peerId = await enr.peerId(); + const address = enr.getLocationMultiaddr('tcp'); + if (address === undefined) { + throw new Error(`Direct peer ${peerId.toString()} has no TCP address, ENR: ${enr.encodeTxt()}`); + } + return { + id: peerId, + addrs: [address], + }; + } catch (err) { + // A malformed configured ENR shouldn't abort preferred-peer setup — skip it and log. + this.logger.warn(`Skipping preferred peer with invalid ENR`, { err }); + return undefined; } - return { - id: peerId, - addrs: [address], - }; }), - ) - ).filter(peer => peer !== undefined); + ), + ); await Promise.all( directPeers.map(peer => { diff --git a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts index 4e1dfecf7244..10436015d589 100644 --- a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts +++ b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts @@ -36,7 +36,12 @@ export class ContractClassPublishedEvent { const version = reader.readField().toNumber(); const artifactHash = reader.readField(); const privateFunctionsRoot = reader.readField(); - const packedPublicBytecode = bufferFromFields(reader.readFieldArray(fieldsWithoutTag.length - reader.cursor)); + const bytecodeFields = reader.readFieldArray(fieldsWithoutTag.length - reader.cursor); + // The fixed-size class log can hold at most this many packed-bytecode bytes (every payload field + // carries 31 bytes). Bound the declared length to it so a malformed log can't declare a multi-MiB + // length backed by a tiny payload and force a large allocation during early (pre-proof) validation. + const maxByteLength = (bytecodeFields.length - 1) * (Fr.SIZE_IN_BYTES - 1); + const packedPublicBytecode = bufferFromFields(bytecodeFields, maxByteLength); return new ContractClassPublishedEvent( contractClassId, diff --git a/yarn-project/prover-node/README.md b/yarn-project/prover-node/README.md index 2e985f8cbab3..d9521c69c340 100644 --- a/yarn-project/prover-node/README.md +++ b/yarn-project/prover-node/README.md @@ -151,12 +151,18 @@ derived from the canonical content for that slot range. stateDiagram-v2 [*] --> initialized initialized --> awaiting_checkpoints: start() - awaiting_checkpoints --> completed: publish succeeds - awaiting_checkpoints --> superseded: longer same-epoch candidate wins - awaiting_checkpoints --> failed: L1 submission errored - awaiting_checkpoints --> cancelled: cancel() + awaiting_checkpoints --> awaiting_root: sub-tree proofs ready, top-tree prove begins + awaiting_root --> publishing_proof: epoch proof ready, submit to L1 + publishing_proof --> completed: publish succeeds + publishing_proof --> superseded: longer same-epoch candidate wins + publishing_proof --> failed: L1 submission errored + awaiting_checkpoints --> failed: top-tree prove errored + awaiting_root --> failed: top-tree prove errored initialized --> timed_out: deadline awaiting_checkpoints --> timed_out: deadline (EpochSession or candidate) + awaiting_root --> timed_out: deadline (EpochSession or candidate) + awaiting_checkpoints --> cancelled: cancel() + awaiting_root --> cancelled: cancel() completed --> [*] superseded --> [*] cancelled --> [*] @@ -164,10 +170,16 @@ stateDiagram-v2 failed --> [*] ``` -The `awaiting-checkpoints` state covers the window between `start()` and the L1 -submission: a `TopTreeJob` is running over the `EpochSession`'s frozen checkpoint set, -awaiting each checkpoint's sub-tree result (`CheckpointProver.whenBlockProofsReady`) -and assembling the epoch proof. +The non-terminal states track the window between `start()` and the L1 submission: + +- `awaiting-checkpoints` — a `TopTreeJob` is awaiting each checkpoint's sub-tree result + (`CheckpointProver.whenBlockProofsReady`) before the top-tree prove can begin. +- `awaiting-root` — the sub-tree proofs are ready and the top-tree (root) prove is running, + assembling the epoch proof (set via the `TopTreeJob` `beforeProve` hook). +- `publishing-proof` — the epoch proof is being submitted to L1 via `ProofPublishingService`. + +`cancel()` and the deadline can fire during any of these pre-submit phases; a terminal state +set that way wins over the phase transitions (which are guarded by `isTerminal()`). The `EpochSession` does three sequential things: (1) run a `TopTreeJob` over the frozen checkpoint subset, (2) hand the resulting proof to `ProofPublishingService` as a diff --git a/yarn-project/prover-node/src/job/epoch-session.test.ts b/yarn-project/prover-node/src/job/epoch-session.test.ts index fea0ebde5345..9a78535ddbfb 100644 --- a/yarn-project/prover-node/src/job/epoch-session.test.ts +++ b/yarn-project/prover-node/src/job/epoch-session.test.ts @@ -19,7 +19,13 @@ import { mock } from 'jest-mock-extended'; import { ProverNodeJobMetrics } from '../metrics.js'; import type { ProofPublishingService, PublishCandidate, PublishOutcome } from '../proof-publishing-service.js'; import { CheckpointProver } from './checkpoint-prover.js'; -import { EpochSession, type EpochSessionDeps, type EpochSessionHooks, type SessionSpec } from './epoch-session.js'; +import { + type EpochProvingJobState, + EpochSession, + type EpochSessionDeps, + type EpochSessionHooks, + type SessionSpec, +} from './epoch-session.js'; import type { TopTreeProof } from './top-tree-job.js'; describe('EpochSession', () => { @@ -325,6 +331,34 @@ describe('EpochSession', () => { }); }); + // ---------------- state reporting ---------------- + + describe('state reporting', () => { + it('advances through awaiting-root (while proving) and publishing-proof (while submitting)', async () => { + let stateDuringProve: EpochProvingJobState | undefined; + let stateDuringSubmit: EpochProvingJobState | undefined; + const session = makeSession({ + hooks: { + // beforeProve has already flipped the state by the time the prove runs. + topTreeProveOverride: () => { + stateDuringProve = session.getState(); + return Promise.resolve(synthProof); + }, + }, + }); + publishingService.submit.mockImplementation(() => { + stateDuringSubmit = session.getState(); + return Promise.resolve('published'); + }); + + const state = await session.start(); + + expect(stateDuringProve).toBe('awaiting-root'); + expect(stateDuringSubmit).toBe('publishing-proof'); + expect(state).toBe('completed'); + }); + }); + // ---------------- helpers ---------------- /** Default session spec used by every test that doesn't override it. */ diff --git a/yarn-project/prover-node/src/job/epoch-session.ts b/yarn-project/prover-node/src/job/epoch-session.ts index 7e4c1ac0f5bc..a6a325ee1ce7 100644 --- a/yarn-project/prover-node/src/job/epoch-session.ts +++ b/yarn-project/prover-node/src/job/epoch-session.ts @@ -92,7 +92,7 @@ export type EpochSessionDeps = { * * Lifecycle (happy path): * - * initialized → awaiting-checkpoints → completed + * initialized → awaiting-checkpoints → awaiting-root → publishing-proof → completed * * Terminal states map the publishing outcome: `published` → `completed`, `superseded` → * `superseded`, `failed` → `failed`, `expired` → `timed-out`, `withdrawn` → `cancelled`. @@ -320,6 +320,12 @@ export class EpochSession implements Traceable { 0, ); + // Reflect the publish phase. Guard against a terminal state set concurrently by cancel() — the + // post-submit isTerminal() check below relies on cancel still winning. + if (!this.isTerminal()) { + this.state = 'publishing-proof'; + } + const outcome = await this.deps.publishingService.submit({ id: this.uuid, epoch: this.spec.epochNumber, @@ -411,15 +417,20 @@ export class EpochSession implements Traceable { } } - private toTopTreeHooks(): TopTreeJobHooks | undefined { + private toTopTreeHooks(): TopTreeJobHooks { const hooks = this.deps.hooks; - if (!hooks?.beforeTopTreeProve && !hooks?.afterTopTreeProve && !hooks?.topTreeProveOverride) { - return undefined; - } return { - beforeProve: hooks.beforeTopTreeProve, - afterProve: hooks.afterTopTreeProve, - proveOverride: hooks.topTreeProveOverride, + // `beforeProve` fires once the sub-tree (checkpoint block) proofs are ready and the root prove is + // about to start — the boundary between `awaiting-checkpoints` and proving the top tree. Don't + // clobber a terminal state set concurrently by cancel(). + beforeProve: async () => { + if (!this.isTerminal()) { + this.state = 'awaiting-root'; + } + await hooks?.beforeTopTreeProve?.(); + }, + afterProve: hooks?.afterTopTreeProve, + proveOverride: hooks?.topTreeProveOverride, }; } } diff --git a/yarn-project/stdlib/src/abi/buffer.test.ts b/yarn-project/stdlib/src/abi/buffer.test.ts index d4ca635d5b92..d73452936ba9 100644 --- a/yarn-project/stdlib/src/abi/buffer.test.ts +++ b/yarn-project/stdlib/src/abi/buffer.test.ts @@ -53,4 +53,9 @@ describe('buffer', () => { expect(result.length).toBe(31); expect(result.toString('hex')).toEqual(buffer.toString('hex')); }); + + it('returns an empty buffer for an empty field array', () => { + const result = bufferFromFields([]); + expect(result.length).toBe(0); + }); }); diff --git a/yarn-project/stdlib/src/abi/buffer.ts b/yarn-project/stdlib/src/abi/buffer.ts index 42d4ea8eba8a..6003f002679d 100644 --- a/yarn-project/stdlib/src/abi/buffer.ts +++ b/yarn-project/stdlib/src/abi/buffer.ts @@ -38,11 +38,23 @@ export function bufferAsFields(input: Buffer, targetLength: number): Fr[] { * bytecode commitment computations to diverge from what the circuit produced. * * @param fields - An output from bufferAsFields: [byteLength, ...payloadFields]. + * @param maxByteLength - Optional upper bound on the declared byte length. When the field array comes + * from untrusted input (e.g. a gossiped contract-class log), pass the payload capacity so an + * attacker-declared length cannot force a large `Buffer.alloc` before the value is otherwise + * rejected. Throws if the declared length exceeds it, before allocating. * @returns A buffer of exactly `byteLength` bytes. */ -export function bufferFromFields(fields: Fr[]): Buffer { +export function bufferFromFields(fields: Fr[], maxByteLength?: number): Buffer { + // A valid bufferAsFields output always has at least the leading byte-length field, so an empty + // input encodes no data. Guard so `length` (the destructured head) is never undefined. + if (fields.length === 0) { + return Buffer.alloc(0); + } const [length, ...payload] = fields; const byteLength = length.toNumber(); + if (maxByteLength !== undefined && byteLength > maxByteLength) { + throw new Error(`Declared byte length ${byteLength} exceeds maximum ${maxByteLength}`); + } const raw = Buffer.concat(payload.map(f => f.toBuffer().subarray(1))); if (raw.length >= byteLength) { return raw.subarray(0, byteLength); diff --git a/yarn-project/stdlib/src/interfaces/prover-node.test.ts b/yarn-project/stdlib/src/interfaces/prover-node.test.ts index e38616738d68..813e244299f4 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.test.ts @@ -40,7 +40,8 @@ class MockProverNode implements ProverNodeApi { return Promise.resolve([ { uuid: 'uuid1', status: 'initialized', epochNumber: 10 }, { uuid: 'uuid2', status: 'awaiting-checkpoints', epochNumber: 10 }, - { uuid: 'uuid3', status: 'awaiting-predecessor', epochNumber: 10 }, + { uuid: 'uuid3', status: 'awaiting-root', epochNumber: 10 }, + { uuid: 'uuid3b', status: 'awaiting-predecessor', epochNumber: 10 }, { uuid: 'uuid4', status: 'publishing-proof', epochNumber: 10 }, { uuid: 'uuid5', status: 'completed', epochNumber: 10 }, { uuid: 'uuid6', status: 'superseded', epochNumber: 10 }, diff --git a/yarn-project/stdlib/src/interfaces/prover-node.ts b/yarn-project/stdlib/src/interfaces/prover-node.ts index 9391b0975f22..e517ffd698d8 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.ts @@ -8,6 +8,7 @@ import { type ComponentsVersions, getVersioningResponseHandler } from '../versio const EpochProvingJobState = [ 'initialized', 'awaiting-checkpoints', + 'awaiting-root', 'awaiting-predecessor', 'publishing-proof', 'completed', diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a06e2d807e2e..e29c09aba20b 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -1774,7 +1774,6 @@ __metadata: typescript: "npm:^5.3.3" uint8arrays: "npm:^5.0.3" viem: "npm:@aztec/viem@2.38.2" - xxhash-wasm: "npm:^1.1.0" languageName: unknown linkType: soft @@ -22055,13 +22054,6 @@ __metadata: languageName: node linkType: hard -"xxhash-wasm@npm:^1.1.0": - version: 1.1.0 - resolution: "xxhash-wasm@npm:1.1.0" - checksum: 10/cad149dabda4ec4ce7c2edd98815c4cc21eadab72f631c832af110066867850fb86b9430499707358d3e23cafe6d71d0a5c16be8f1864f213e4cd1aa74bd9556 - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8"