diff --git a/.claude/agents/aztec-wallet.md b/.claude/agents/aztec-wallet.md index 316449270476..293a58c6e467 100644 --- a/.claude/agents/aztec-wallet.md +++ b/.claude/agents/aztec-wallet.md @@ -48,7 +48,7 @@ fi # Query node version RESPONSE=$(curl -sf -X POST -H 'Content-type: application/json' \ - --data '{"jsonrpc":"2.0","id":1,"method":"node_getNodeInfo"}' \ + --data '{"jsonrpc":"2.0","id":1,"method":"aztec_getNodeInfo"}' \ "$RPC_URL" 2>&1) || { echo "ERROR: Could not reach node at $RPC_URL" >&2 echo "Response: $RESPONSE" >&2 diff --git a/.claude/skills/release-docs/SKILL.md b/.claude/skills/release-docs/SKILL.md index 6eb78293f32d..d7b960a0ca3a 100644 --- a/.claude/skills/release-docs/SKILL.md +++ b/.claude/skills/release-docs/SKILL.md @@ -30,7 +30,7 @@ Fetch node info from the provided RPC URL: ```bash curl -s -X POST -H 'Content-Type: application/json' \ - -d '{"method":"node_getNodeInfo"}' | jq .result + -d '{"method":"aztec_getNodeInfo"}' | jq .result ``` Parse the response to extract: @@ -214,7 +214,7 @@ These files are auto-generated — do not hand-edit them. Regenerate the Node JSON-RPC API reference documentation. This script parses the TypeScript interface definitions and Zod schemas in `yarn-project/stdlib/src/interfaces/` -to produce a complete markdown reference for the `node_` and `nodeAdmin_` RPC methods. +to produce a complete markdown reference for the `aztec_` and `aztecAdmin_` RPC methods. **Prerequisite:** `yarn-project` must be built (already done in Step 6 prerequisites). @@ -259,7 +259,7 @@ docs (Step 13), the generated content is included in the snapshot automatically. ### Step 9: Resolve Missing Contract Addresses & Update Network Info The `networks.md` L1 table includes contracts that are **not** returned by -`node_getNodeInfo`. Before updating the tables, resolve these in three tiers. +`aztec_getNodeInfo`. Before updating the tables, resolve these in three tiers. Determine the L1 RPC URL from the `l1ChainId`: `1` → Ethereum mainnet, `11155111` → Sepolia. The Rollup and Registry addresses are already known from diff --git a/.claude/skills/release-network-docs/SKILL.md b/.claude/skills/release-network-docs/SKILL.md index 0307e1c2f4d6..884801e557cc 100644 --- a/.claude/skills/release-network-docs/SKILL.md +++ b/.claude/skills/release-network-docs/SKILL.md @@ -45,7 +45,7 @@ Fetch node info from the provided RPC URL: ```bash curl -s -X POST -H 'Content-Type: application/json' \ - -d '{"method":"node_getNodeInfo"}' | jq .result + -d '{"method":"aztec_getNodeInfo"}' | jq .result ``` Parse the response to extract: @@ -87,7 +87,7 @@ git tag -l "v" ### Step 3: Identify and Resolve Missing Contract Addresses The `networks.md` L1 table includes contracts that are **not** returned by -`node_getNodeInfo`. Resolve these addresses in three tiers: +`aztec_getNodeInfo`. Resolve these addresses in three tiers: #### Tier 1: Query on-chain from known contracts diff --git a/.test_patterns.yml b/.test_patterns.yml index b740808ec72b..498fc75e22ad 100644 --- a/.test_patterns.yml +++ b/.test_patterns.yml @@ -371,7 +371,7 @@ tests: owners: - *palla - - regex: "yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.test.ts" + - regex: "yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.parallel.test.ts" owners: - *spyros diff --git a/docs/README.md b/docs/README.md index 0276b0976e0b..f1d3bd85850b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -276,8 +276,8 @@ The Node JSON-RPC API reference is auto-generated from the TypeScript interface **Source files:** -- `yarn-project/stdlib/src/interfaces/aztec-node.ts` — `AztecNode` interface (`node_` methods) -- `yarn-project/stdlib/src/interfaces/aztec-node-admin.ts` — `AztecNodeAdmin` interface (`nodeAdmin_` methods) +- `yarn-project/stdlib/src/interfaces/aztec-node.ts` — `AztecNode` interface (`aztec_` methods) +- `yarn-project/stdlib/src/interfaces/aztec-node-admin.ts` — `AztecNodeAdmin` interface (`aztecAdmin_` methods) - `yarn-project/stdlib/src/block/l2_block_source.ts` — `L2BlockSource` interface (JSDoc for inherited methods) **Prerequisites:** `yarn-project` must have `node_modules/` installed so `npx tsx` can resolve `typescript`. Run `yarn install` from `yarn-project` if needed. No build is required — the generator parses source `.ts` files via the TypeScript Compiler API, not compiled output. diff --git a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md index 5f5d9d1cb6f6..5f6c2df87661 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md +++ b/docs/docs-developers/docs/aztec-js/how_to_pay_fees.md @@ -38,25 +38,25 @@ Mana is Aztec's unit of computational effort (like gas on Ethereum), and Fee Jui When using `EmbeddedWallet`, gas is estimated automatically on every `send()` call. You only need to manually estimate if you want to preview costs before sending, or if you're using a custom wallet implementation. ::: -Before sending a transaction, you can estimate the mana it will consume by simulating with `estimateGas: true`: +Before sending a transaction, you can read the mana it will consume by simulating with `includeMetadata: true` and reading `gasUsed` from the result: #include_code estimate_mana /docs/examples/ts/aztecjs_advanced/index.ts typescript -The `estimatedGas` object contains: +The `gasUsed` object contains the raw gas the simulation consumed: -- `gasLimits.daGas` - Estimated DA mana for main execution -- `gasLimits.l2Gas` - Estimated L2 mana for main execution -- `teardownGasLimits.daGas` - Estimated DA mana for teardown phase -- `teardownGasLimits.l2Gas` - Estimated L2 mana for teardown phase +- `totalGas.daGas` / `totalGas.l2Gas` - DA and L2 mana consumed across the whole transaction +- `teardownGas.daGas` / `teardownGas.l2Gas` - DA and L2 mana consumed in the teardown phase + +It is up to you to derive the gas limits you declare from this raw usage (typically by padding it, as shown above). If you don't declare any gas limits, the wallet fills in the network's per-tx admission limits for you. ### Calculate expected fee from estimate -To calculate the expected fee from estimated gas, use the `computeFee` method with current network fees: +To calculate the expected fee from the padded gas, use the `computeFee` method with current network fees: #include_code compute_fee_from_estimate /docs/examples/ts/aztecjs_advanced/index.ts typescript :::tip -The `estimatedGasPadding` parameter adds a safety margin to the estimate. A value of `0.1` adds 10% padding. Use higher padding for transactions with variable gas costs. +Pad the raw `gasUsed` yourself to leave a safety margin. Multiplying by `1.1` adds 10%; use higher padding for transactions with variable gas costs. The wallet rejects any declared limit above the network's per-tx admission limit. ::: ## Get transaction fee from receipt @@ -148,7 +148,7 @@ const payment = await fpcClient.createPaymentMethod({ wallet, user: aliceAddress, // the account paying the fee tokenAddress, // the token you want to pay in - estimatedGas, // from a prior estimateGas call + estimatedGas, // gas limits derived from a prior simulation's gasUsed }); // Use it like any other payment method @@ -171,7 +171,7 @@ Fee Juice is non-transferable on L2, but you can bridge it from L1, claim it on #include_code bridge_fee_juice_setup /docs/examples/ts/aztecjs_connection/index.ts typescript -Under the hood, `L1FeeJuicePortalManager` gets the L1 addresses from the node `node_getNodeInfo` endpoint. It then exposes an easy method `bridgeTokensPublic` which mints fee juice on L1 and sends it to an L2 address via the L1 portal: +Under the hood, `L1FeeJuicePortalManager` gets the L1 addresses from the node `aztec_getNodeInfo` endpoint. It then exposes an easy method `bridgeTokensPublic` which mints fee juice on L1 and sends it to an L2 address via the L1 portal: #include_code bridge_fee_juice_execute /docs/examples/ts/aztecjs_connection/index.ts typescript @@ -207,7 +207,7 @@ Note that `gasLimits` and `teardownGasLimits` use `daGas`/`l2Gas` field names, w ### Use automatic gas estimation :::note -When using `EmbeddedWallet`, gas estimation happens automatically on every `send()`; you don't need to pass `estimateGas`. This option is useful for custom wallet implementations or when you want to estimate gas during a `simulate()` call. +When using `EmbeddedWallet`, gas estimation happens automatically on every `send()`; you don't need to declare gas limits at all. Reading `gasUsed` from a `simulate({ includeMetadata: true })` result is useful when you want to preview costs before sending, or to set explicit limits with a custom wallet implementation. ::: #include_code auto_gas_estimation /docs/examples/ts/aztecjs_advanced/index.ts typescript diff --git a/docs/docs-developers/docs/aztec-js/how_to_read_data.md b/docs/docs-developers/docs/aztec-js/how_to_read_data.md index a9e03a51b79f..30152a46b50d 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_read_data.md +++ b/docs/docs-developers/docs/aztec-js/how_to_read_data.md @@ -39,7 +39,7 @@ Set `includeMetadata: true` to get additional information about the simulation: #include_code simulate_with_metadata /docs/examples/ts/aztecjs_advanced/index.ts typescript -The result includes `result` (the function return value), `stats` (execution statistics), `offchainEffects`, and `estimatedGas` (with `gasLimits` and `teardownGasLimits`). +The result includes `result` (the function return value), `stats` (execution statistics), `offchainEffects`, and `gasUsed` (the raw gas the simulation consumed, with `totalGas` and `teardownGas`). Derive your own gas limits from `gasUsed` if you want to declare them explicitly; otherwise the wallet fills in the network's per-tx admission limits. ### Private function considerations diff --git a/docs/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md b/docs/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md index 55a5f0ec45c8..036c68e106f6 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md +++ b/docs/docs-developers/docs/aztec-js/how_to_use_private_fee_juice.md @@ -59,7 +59,7 @@ Cold-start exists for users who have no other way to pay fees: the bridged amoun Because neither `pay_fee` nor `mint_and_pay_fee` makes public cross-contract token calls in setup (they only deduct from the FPC's internal private balance and invoke `set_as_fee_payer`), the [setup-phase allowlist](../foundational-topics/transactions.md#setup-phase-non-revertible) never blocks these flows. :::note No refund -`PrivateFPC.pay_fee()` deducts the full `max_gas_cost` and does not refund unused gas. Use `estimateGas` (see [Estimate mana costs](./how_to_pay_fees.md#estimate-mana-costs)) to right-size your limits. +`PrivateFPC.pay_fee()` deducts the full `max_gas_cost` and does not refund unused gas. Read `gasUsed` from a simulation (see [Estimate mana costs](./how_to_pay_fees.md#estimate-mana-costs)) to right-size your limits. ::: ## Share one FPC address across the ecosystem diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 3410cc18587f..da3a5640ec19 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,68 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.js] Wallets validate declared gas limits against the network's per-tx admission limit + +Wallets now reject a transaction whose declared `gasLimits` exceed the network's per-tx admission limit (the node-advertised `txsLimits.gas`), throwing before the tx is sent — e.g. `Declared DA gas limit (X) exceeds the maximum this network allows per tx (Y)`. When you declare no gas limits, the wallet fills in the network's admission limits for you. This mirrors the node's inbound `GasLimitsValidator`, surfacing the rejection locally instead of on submission. + +**Impact**: Transactions that previously over-declared gas (and were silently skipped by the proposer) now fail fast with a descriptive error. Declare limits at or below `txsLimits.gas`, or declare none and let the wallet fill them in. + +### [Aztec.js] `estimateGas` / `estimatedGasPadding` simulate options and the `estimatedGas` result field removed + +The `estimateGas` and `estimatedGasPadding` fee options are gone, and the `estimatedGas` field on simulation results is replaced by `gasUsed` (the raw gas the simulation consumed). Apps that want explicit gas limits read `gasUsed` and pad it themselves; otherwise the wallet fills in the network's admission limits automatically. + +**Migration:** + +```diff +- const { estimatedGas } = await contract.methods.foo(args).simulate({ +- from, +- fee: { estimateGas: true, estimatedGasPadding: 0.1 }, +- }); +- const gasLimits = estimatedGas.gasLimits; ++ const { gasUsed } = await contract.methods.foo(args).simulate({ from, includeMetadata: true }); ++ const gasLimits = gasUsed.totalGas.mul(1.1); // pad yourself +``` + +### [Aztec.js] `getGasLimits` moved to `@aztec/wallet-sdk` and is no longer exported from `@aztec/aztec.js` + +`getGasLimits` is no longer exported from `@aztec/aztec.js`. It now lives in `@aztec/wallet-sdk/base-wallet`, takes the simulated `gasUsed` and the network's per-tx admission limit, and clamps the padded estimates to it. If the simulated usage already exceeds the network limit, it throws immediately rather than returning a limit the node would reject. + +**Migration:** + +```diff +- import { getGasLimits } from '@aztec/aztec.js'; +- const { gasLimits, teardownGasLimits } = getGasLimits(simulationResult, 0.1); ++ import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; ++ const { txsLimits } = await node.getNodeInfo(); ++ const { gasLimits, teardownGasLimits } = getGasLimits(simulationResult.gasUsed, Gas.from(txsLimits.gas), 0.1); +``` + +### [Aztec.js / PXE] `NodeInfo.txsLimits` is now required + +`NodeInfo` now carries a required `txsLimits` field: every node advertises the maximum gas a single tx may declare (`{ gas: { daGas, l2Gas } }`) and wallets rely on it for fallback gas limits. Clients built against this version cannot talk to nodes that predate the field. + +### [Aztec.js] `GasSettings.fallback` requires explicit `gasLimits` + +`GasSettings.fallback` no longer supplies a default value for `gasLimits`. Callers must pass the network's per-tx admission limit explicitly — read it from the node's `txsLimits.gas`. + +**Migration:** + +```diff +- const settings = GasSettings.fallback({ maxFeesPerGas }); ++ const { txsLimits } = await node.getNodeInfo(); ++ const settings = GasSettings.fallback({ gasLimits: Gas.from(txsLimits.gas), maxFeesPerGas }); +``` + +### [Aztec.js / stdlib] Removed legacy fallback gas constants + +The following exports have been removed from `@aztec/stdlib`: + +- `APPROXIMATE_MAX_DA_GAS_PER_BLOCK` +- `FALLBACK_TEARDOWN_L2_GAS_LIMIT` +- `FALLBACK_TEARDOWN_DA_GAS_LIMIT` + +**Impact**: Any code that imported these symbols must switch to the live node-advertised limits via the node's `txsLimits.gas`. + ### [Aztec.nr] `messages::message_delivery` module moved to `messages::delivery` The `message_delivery` module has been renamed to `delivery`. Update imports accordingly: @@ -18,6 +80,19 @@ The `message_delivery` module has been renamed to `delivery`. Update imports acc + use aztec::messages::delivery::MessageDelivery; ``` +### [Node JSON-RPC] Method prefixes changed to `aztec_*` and `aztecAdmin_*` + +All Aztec node JSON-RPC method prefixes have changed: + +- `node_*` → `aztec_*` (public node methods, port 8080) +- `nodeAdmin_*` → `aztecAdmin_*` (admin methods, port 8880) +- `nodeDebug_*` → `aztecDebug_*` (debug methods, port 8080, local-network or `--node-debug` only) +- `p2p_*` namespace removed; P2P queries are on `aztec_*`: `getPeers`, `getCheckpointAttestationsForSlot`, `getProposalsForSlot` +- New archiver sync helpers on `aztec_*`: `getL1Constants`, `getSyncedL2SlotNumber`, `getSyncedL2EpochNumber`, `getSyncedL1Timestamp` + +If you call the node RPC directly (e.g. via `curl` or a custom client), update all method names accordingly. +Clients created via `createAztecNodeClient`, `createAztecNodeAdminClient`, and `createAztecNodeDebugClient` are updated automatically. + ### [Aztec.nr] `get_pending_tagged_logs` oracle interface updated (oracle version 28) The `aztec_utl_getPendingTaggedLogs` oracle now takes an additional `provided_secrets` parameter of type `EphemeralArray`. This lets apps pass tagging secrets that PXE cannot derive on its own (e.g. handshake-derived secrets) alongside the secrets PXE manages internally. @@ -100,7 +175,7 @@ After the `auth_registry`, `public_checks`, and `multi_call_entrypoint` demotion ### [Aztec.nr] `multi_call_entrypoint` demoted from protocol contract -`multi_call_entrypoint` is no longer a protocol contract; its address is derived from its artifact rather than hardcoded at `6`, and PXE no longer auto-registers it. It is now a standard contract that PXE *preloads*: both `createPXE` and `EmbeddedWallet` preload the standard MultiCallEntrypoint automatically (and `EmbeddedWallet` additionally preloads `AuthRegistry`). **If you use the standard PXE or `EmbeddedWallet`, no changes are needed** — multicall keeps working out of the box. +`multi_call_entrypoint` is no longer a protocol contract; its address is derived from its artifact rather than hardcoded at `6`, and PXE no longer auto-registers it. It is now a standard contract that PXE _preloads_: both `createPXE` and `EmbeddedWallet` preload the standard MultiCallEntrypoint automatically (and `EmbeddedWallet` additionally preloads `AuthRegistry`). **If you use the standard PXE or `EmbeddedWallet`, no changes are needed** — multicall keeps working out of the box. To preload a different set of standard contracts (for example to also preload `PublicChecks`, which is not preloaded by default), a wallet or app passes its own `preloadedContractsProvider` through the wallet's PXE options: @@ -121,7 +196,7 @@ const wallet = await EmbeddedWallet.create(node, { }); ``` -The provider *replaces* the default list (it is not additive), so include every standard contract you want available. +The provider _replaces_ the default list (it is not additive), so include every standard contract you want available. ### [Aztec.nr] `public_checks` demoted from protocol contract diff --git a/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md b/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md index d57fa538d4d6..3c73a8bb54df 100644 --- a/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md +++ b/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md @@ -43,7 +43,7 @@ Wait for output showing deployed contract addresses. To get the **Registry Addre ```bash curl -s http://localhost:8080 -X POST -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"node_getNodeInfo","params":[],"id":1}' | jq '.result.l1ContractAddresses' + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' | jq '.result.l1ContractAddresses' ``` Note the `registryAddress` from the output. diff --git a/docs/docs-developers/getting_started_on_local_network.md b/docs/docs-developers/getting_started_on_local_network.md index 15905aadb70b..38492b7107cc 100644 --- a/docs/docs-developers/getting_started_on_local_network.md +++ b/docs/docs-developers/getting_started_on_local_network.md @@ -12,11 +12,14 @@ Get started on your local environment using a local network. If you'd rather dep The local network is a local development Aztec network running fully on your machine, and interacting with a development Ethereum node. You can develop and deploy on it just like on a testnet or mainnet (when the time comes). The local network makes it faster and easier to develop and test your Aztec applications. +The local network always owns the local chain it starts. It deploys its own Aztec protocol contracts to the local Ethereum node and is not a mode for connecting to an existing Aztec network. + What's included in the local network: - Local Ethereum network (Anvil) - Deployed Aztec protocol contracts (for L1 and L2) - A set of test accounts with some test tokens to pay fees +- On-demand block production via the automine sequencer - Development tools to compile contracts and interact with the network (`aztec` and `aztec-wallet`) This guide will teach you how to install the Aztec local network, run it using the Aztec CLI, and interact with contracts using the wallet CLI. To jump right into the testnet instead, click the `Testnet` tab. diff --git a/docs/docs-operate/operators/reference/node-api-reference.md b/docs/docs-operate/operators/reference/node-api-reference.md index 5e50d2caad10..75f77a9714bc 100644 --- a/docs/docs-operate/operators/reference/node-api-reference.md +++ b/docs/docs-operate/operators/reference/node-api-reference.md @@ -19,11 +19,11 @@ This document provides a complete reference for the Aztec Node JSON RPC API. All Note that the above ports are only defaults, and can be modified by setting `--port` and `--admin-port` flags upon startup. -All methods use standard JSON RPC 2.0 format with methods prefixed by `node_` or `nodeAdmin_`. +All methods use standard JSON RPC 2.0 format with methods prefixed by `aztec_` or `aztecAdmin_`. ## Block queries -### node_getBlockNumber +### aztec_getBlockNumber Returns the block number at a given chain tip, or the latest proposed block number when `tip` is omitted. @@ -39,10 +39,10 @@ Returns the block number at a given chain tip, or the latest proposed block numb ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlockNumber","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getBlockNumber","params":["0x1234..."],"id":1}' ``` -### node_getCheckpointNumber +### aztec_getCheckpointNumber Returns the checkpoint number at a given chain tip, or the latest checkpoint number when `tip` is omitted. @@ -62,10 +62,10 @@ checkpoints are not exposed over RPC. `'checkpointed'` on the checkpoint side is ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getCheckpointNumber","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointNumber","params":["0x1234..."],"id":1}' ``` -### node_getChainTips +### aztec_getChainTips Returns the tips of the L2 chain. @@ -78,10 +78,74 @@ Returns the tips of the L2 chain. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getChainTips","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":1}' ``` -### node_getBlock +### aztec_getL1Constants + +Returns the rollup constants for the current chain. + +**Parameters**: None + +**Returns**: `L1RollupConstants` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1Constants","params":[],"id":1}' +``` + +### aztec_getSyncedL2SlotNumber + +Returns the last L2 slot number for which the node has all L1 data needed to build the next checkpoint. + +**Parameters**: None + +**Returns**: `SlotNumber | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL2SlotNumber","params":[],"id":1}' +``` + +### aztec_getSyncedL2EpochNumber + +Returns the last L2 epoch number that has been fully synchronized from L1. + +**Parameters**: None + +**Returns**: `number | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL2EpochNumber","params":[],"id":1}' +``` + +### aztec_getSyncedL1Timestamp + +Returns the latest L1 timestamp according to the archiver's synced L1 view. + +**Parameters**: None + +**Returns**: `bigint | undefined` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getSyncedL1Timestamp","params":[],"id":1}' +``` + +### aztec_getBlock Unified block fetch. Returns the block identified by `param`, with optional fields controlled by `options`. @@ -98,10 +162,10 @@ by `options`. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlock","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getBlock","params":["latest","0x1234..."],"id":1}' ``` -### node_getBlockData +### aztec_getBlockData Lightweight block-metadata fetch. Returns the block identified by `param` without transaction bodies or other optional context. Cheaper than `getBlock` for header-only access. @@ -117,10 +181,10 @@ bodies or other optional context. Cheaper than `getBlock` for header-only access ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlockData","params":["latest"],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getBlockData","params":["latest"],"id":1}' ``` -### node_getBlocks +### aztec_getBlocks Returns up to `limit` blocks starting from `from`, projected to the shape determined by `options`. @@ -138,10 +202,10 @@ shape determined by `options`. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlocks","params":[1,100,"0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getBlocks","params":[1,100,"0x1234..."],"id":1}' ``` -### node_getCheckpoint +### aztec_getCheckpoint Unified checkpoint fetch. Returns the checkpoint identified by `param`, with optional fields controlled by `options`. @@ -158,10 +222,10 @@ controlled by `options`. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getCheckpoint","params":["0x1234...","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpoint","params":["0x1234...","0x1234..."],"id":1}' ``` -### node_getCheckpoints +### aztec_getCheckpoints Returns up to `limit` checkpoints starting from `from`, projected to the shape determined by `options`. @@ -179,10 +243,10 @@ Returns up to `limit` checkpoints starting from `from`, projected to the ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getCheckpoints","params":[1,100,"0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpoints","params":[1,100,"0x1234..."],"id":1}' ``` -### node_getCheckpointsData +### aztec_getCheckpointsData Gets lightweight checkpoint metadata for a contiguous range or for an entire epoch. @@ -197,12 +261,12 @@ Gets lightweight checkpoint metadata for a contiguous range or for an entire epo ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getCheckpointsData","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointsData","params":["0x1234..."],"id":1}' ``` ## Transaction operations -### node_sendTx +### aztec_sendTx Method to submit a transaction to the p2p pool. @@ -217,18 +281,21 @@ Method to submit a transaction to the p2p pool. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_sendTx","params":[{"data":"0x..."}],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_sendTx","params":[{"data":"0x..."}],"id":1}' ``` -### node_getTxReceipt +### aztec_getTxReceipt -Fetches a transaction receipt for a given transaction hash. Returns a mined receipt if it was added -to the chain, a pending receipt if it's still in the mempool of the connected Aztec node, or a dropped -receipt if not found in the connected Aztec node. +Fetches a transaction receipt for a given transaction hash. Always resolves to one of the lifecycle variants of +the union: a if the tx was included in a block, a +if it's still in the mempool of the connected Aztec node, or a if not found. **Parameters**: 1. `txHash` - `TxHash` - The transaction hash. +2. `options` - `GetTxReceiptOptions | undefined` - Optional flags controlling which extra data is attached: `includeTxEffect` attaches the full + to a mined receipt, `includePendingTx` attaches the pending to a pending receipt, and +`includeProof` keeps the proof on that attached pending tx (only meaningful with `includePendingTx`). **Returns**: `TxReceipt` - A receipt of the transaction. @@ -237,13 +304,15 @@ receipt if not found in the connected Aztec node. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getTxReceipt","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getTxReceipt","params":["0x1234...","0x1234..."],"id":1}' ``` -### node_getTxEffect +### aztec_getTxEffect Gets a tx effect. +**Deprecated**: Use `getTxReceipt(txHash, { includeTxEffect: true })` and read the `.txEffect` field instead. + **Parameters**: 1. `txHash` - `TxHash` - The hash of the tx corresponding to the tx effect. @@ -255,16 +324,17 @@ Gets a tx effect. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getTxEffect","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getTxEffect","params":["0x1234..."],"id":1}' ``` -### node_getTxByHash +### aztec_getTxByHash -Method to retrieve a single pending tx. +Method to retrieve a single pending tx. The tx's proof is stripped unless `includeProof` is set. **Parameters**: 1. `txHash` - `TxHash` - The transaction hash to return. +2. `options` - `GetTxByHashOptions | undefined` - Options for the returned tx (eg whether to include its proof). **Returns**: `Tx | undefined` - The pending tx if it exists. @@ -273,16 +343,17 @@ Method to retrieve a single pending tx. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getTxByHash","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getTxByHash","params":["0x1234...","0x1234..."],"id":1}' ``` -### node_getTxsByHash +### aztec_getTxsByHash -Method to retrieve multiple pending txs. +Method to retrieve multiple pending txs. The txs' proofs are stripped unless `includeProof` is set. **Parameters**: 1. `txHashes` - `TxHash[]` +2. `options` - `GetTxByHashOptions | undefined` - Options for the returned txs (eg whether to include their proofs). **Returns**: `Tx[]` - The pending txs if exist. @@ -291,17 +362,18 @@ Method to retrieve multiple pending txs. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getTxsByHash","params":[["0x1234..."]],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getTxsByHash","params":[["0x1234..."],"0x1234..."],"id":1}' ``` -### node_getPendingTxs +### aztec_getPendingTxs -Method to retrieve pending txs. +Method to retrieve pending txs. The txs' proofs are stripped unless `includeProof` is set. **Parameters**: -1. `limit` - `number | undefined` -2. `after` - `TxHash | undefined` +1. `limit` - `number | undefined` - The number of items to return. +2. `after` - `TxHash | undefined` - The last known pending tx. Used for pagination. +3. `options` - `GetTxByHashOptions | undefined` - Options for the returned txs (eg whether to include their proofs). **Returns**: `Tx[]` - The pending txs. @@ -310,10 +382,10 @@ Method to retrieve pending txs. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPendingTxs","params":[100,"0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPendingTxs","params":[100,"0x1234...","0x1234..."],"id":1}' ``` -### node_getPendingTxCount +### aztec_getPendingTxCount Retrieves the number of pending txs @@ -326,10 +398,10 @@ Retrieves the number of pending txs ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPendingTxCount","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPendingTxCount","params":[],"id":1}' ``` -### node_isValidTx +### aztec_isValidTx Returns true if the transaction is valid for inclusion at the current state. Valid transactions can be made invalid by *other* transactions if e.g. they emit the same nullifiers, or come become invalid @@ -347,10 +419,10 @@ due to e.g. the expiration_timestamp property. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_isValidTx","params":[{"data":"0x..."},{}],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_isValidTx","params":[{"data":"0x..."},{}],"id":1}' ``` -### node_simulatePublicCalls +### aztec_simulatePublicCalls Simulates the public part of a transaction with the current state. This currently just checks that the transaction execution succeeds. @@ -358,7 +430,9 @@ This currently just checks that the transaction execution succeeds. **Parameters**: 1. `tx` - `Tx` - The transaction to simulate. -2. `skipFeeEnforcement` - `boolean | undefined` +2. `skipFeeEnforcement` - `boolean | undefined` - If true, fee enforcement is skipped. +3. `overrides` - `SimulationOverrides | undefined` - Optional pre-simulation overrides applied to the ephemeral fork and contract DB +(publicStorage writes, contract instance overrides). **Returns**: `PublicSimulationOutput` @@ -367,12 +441,12 @@ This currently just checks that the transaction execution succeeds. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_simulatePublicCalls","params":[{"data":"0x..."},true],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_simulatePublicCalls","params":[{"data":"0x..."},true,"0x1234..."],"id":1}' ``` ## State queries -### node_getPublicStorageAt +### aztec_getPublicStorageAt Gets the storage value at the given contract storage slot. @@ -392,10 +466,10 @@ Aztec's version of `eth_getStorageAt`. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPublicStorageAt","params":["latest","0x1234...","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPublicStorageAt","params":["latest","0x1234...","0x1234..."],"id":1}' ``` -### node_getWorldStateSyncStatus +### aztec_getWorldStateSyncStatus Returns the sync status of the node's world state @@ -408,12 +482,12 @@ Returns the sync status of the node's world state ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getWorldStateSyncStatus","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getWorldStateSyncStatus","params":[],"id":1}' ``` ## Membership witnesses -### node_findLeavesIndexes +### aztec_findLeavesIndexes Find the indexes of the given leaves in the given tree along with a block metadata pointing to the block in which the leaves were inserted. @@ -431,10 +505,10 @@ the leaves were inserted. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_findLeavesIndexes","params":["latest",1,["0x1234..."]],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_findLeavesIndexes","params":["latest",1,["0x1234..."]],"id":1}' ``` -### node_getNullifierMembershipWitness +### aztec_getNullifierMembershipWitness Returns a nullifier membership witness for a given nullifier at a given block. @@ -450,10 +524,10 @@ Returns a nullifier membership witness for a given nullifier at a given block. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` -### node_getLowNullifierMembershipWitness +### aztec_getLowNullifierMembershipWitness Returns a low nullifier membership witness for a given nullifier at a given block. @@ -473,10 +547,10 @@ we are trying to prove non-inclusion for. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getLowNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getLowNullifierMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` -### node_getPublicDataWitness +### aztec_getPublicDataWitness Returns a public data tree witness for a given leaf slot at a given block. @@ -496,10 +570,10 @@ is contained in the leaf preimage. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPublicDataWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPublicDataWitness","params":["latest","0x1234..."],"id":1}' ``` -### node_getBlockHashMembershipWitness +### aztec_getBlockHashMembershipWitness Returns a membership witness for a given block hash in the archive tree. @@ -521,10 +595,10 @@ a specific block exists in the chain's history. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlockHashMembershipWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getBlockHashMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` -### node_getNoteHashMembershipWitness +### aztec_getNoteHashMembershipWitness Returns a membership witness for a given note hash at a given block. @@ -540,12 +614,12 @@ Returns a membership witness for a given note hash at a given block. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getNoteHashMembershipWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getNoteHashMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` ## L1 to L2 messages -### node_getL1ToL2MessageMembershipWitness +### aztec_getL1ToL2MessageMembershipWitness Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree. @@ -561,10 +635,10 @@ Returns the index and a sibling path for a leaf in the committed l1 to l2 data t ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getL1ToL2MessageMembershipWitness","params":["latest","0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` -### node_getL1ToL2MessageCheckpoint +### aztec_getL1ToL2MessageCheckpoint Returns the L2 checkpoint number in which this L1 to L2 message becomes available, or undefined if not found. @@ -579,37 +653,45 @@ Returns the L2 checkpoint number in which this L1 to L2 message becomes availabl ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getL1ToL2MessageCheckpoint","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageCheckpoint","params":["0x1234..."],"id":1}' ``` -### node_getL2ToL1MembershipWitness +### aztec_getL2ToL1MembershipWitness -Returns the L2-to-L1 membership witness for a message emitted by a transaction. +Returns the L2-to-L1 membership witness for `message` emitted by tx `txHash`. The node selects +the smallest partial-proof root on the Outbox that covers the tx's checkpoint and builds the +witness against it. + +The node reads the Outbox roots lazily, pinned to its synced L1 block, so the witness reflects +the node's synced view. Returns `undefined` if the tx isn't yet in a block/epoch or no covering +root has landed on L1 as of the synced block. + +Caveat: cached roots that are sealed and L1-finalized are not re-validated. A reorg deeper than +L1 finality could leave the node serving a witness against a no-longer-canonical root. **Parameters**: -1. `txHash` - `TxHash` - The transaction that emitted the L2-to-L1 message. +1. `txHash` - `TxHash` - The tx whose L2-to-L1 message we want a witness for. 2. `message` - `Fr` - The message hash to prove inclusion of. -3. `messageIndexInTx` - `number` - Optional index of the message within the transaction. Use this when the same message hash appears multiple times in the transaction. +3. `messageIndexInTx` - `object | undefined` - Optional index of the message within the tx's L2-to-L1 messages; pass +this when the same message hash appears multiple times in the tx. -**Returns**: `L2ToL1MembershipWitness | undefined` - The membership witness, or undefined if the transaction is not settled or no covering Outbox root is available yet. +**Returns**: `L2ToL1MembershipWitness | undefined` **Example**: ```bash curl -X POST http://localhost:8080 \ - -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"method\":\"node_getL2ToL1MembershipWitness\",\"params\":[\"0x1234...\",\"0xabcd...\"],\"id\":1}" + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL2ToL1MembershipWitness","params":["0x1234...","0x1234...",{}],"id":1}' ``` -### node_getL2ToL1Messages - -:::warning Deprecated -Use `node_getL2ToL1MembershipWitness` to get an L2-to-L1 message witness directly. -::: +### aztec_getL2ToL1Messages Returns all the L2 to L1 messages in an epoch. +**Deprecated**: Use to get an L2-to-L1 message witness directly. + **Parameters**: 1. `epoch` - `number` - The epoch at which to get the data. @@ -622,101 +704,61 @@ array if the epoch is not found). ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getL2ToL1Messages","params":[12345],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getL2ToL1Messages","params":[12345],"id":1}' ``` ## Log queries -### node_getPublicLogs - -Gets public logs based on the provided filter. - -**Parameters**: - -1. `filter` - `LogFilter` - The filter to apply to the logs. +### aztec_getPrivateLogsByTags -**Returns**: `GetPublicLogsResponse` - The requested logs. +Gets private logs matching the given tags. Returns one inner array per element of `query.tags`, in +input order. An empty inner array means no logs matched that tag. Set `query.includeEffects` to also +receive the tx's note hashes and nullifiers. -**Example**: - -```bash -curl -X POST http://localhost:8080 \ - -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPublicLogs","params":[{"fromBlock":100,"toBlock":200}],"id":1}' -``` - -### node_getContractClassLogs - -Gets contract class logs based on the provided filter. +The return type is the widest shape — `noteHashes`/`nullifiers` are typed as +optional even when `includeEffects: true` is set. JSON-RPC validation can't preserve a stricter +narrowing across the wire. Callers that want a narrowed type at the call site should use the typed +helpers in `pxe/src/tagging/get_all_logs_by_tags.ts`. **Parameters**: -1. `filter` - `LogFilter` - The filter to apply to the logs. +1. `query` - `PrivateLogsQuery` -**Returns**: `GetContractClassLogsResponse` - The requested logs. +**Returns**: `LogResult[][]` **Example**: ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getContractClassLogs","params":[{"fromBlock":100,"toBlock":200}],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPrivateLogsByTags","params":["0x1234..."],"id":1}' ``` -### node_getPrivateLogsByTags - -Gets private logs that match any of the `tags`. For each tag, an array of matching logs is returned. An empty -array implies no logs match that tag. - -**Parameters**: - -1. `tags` - `SiloedTag[]` - The tags to search for. -2. `page` - `number | undefined` - The page number (0-indexed) for pagination. -3. `referenceBlock` - `BlockHash | undefined` - Optional block hash used to ensure the block still exists before logs are retrieved. -This block is expected to represent the latest block to which the client has synced (called anchor block in PXE). -If specified and the block is not found, an error is thrown. This helps detect reorgs, which could result in -undefined behavior in the client's code. - -**Returns**: `TxScopedL2Log[][]` - An array of log arrays, one per tag. Returns at most 10 logs per tag per page. If 10 logs are returned -for a tag, the caller should fetch the next page to check for more logs. - -**Example**: - -```bash -curl -X POST http://localhost:8080 \ - -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPrivateLogsByTags","params":[["0x1234..."],0,"0x1234..."],"id":1}' -``` +### aztec_getPublicLogsByTags -### node_getPublicLogsByTagsFromContract +Gets public logs matching the given tags for the given contract. Returns one inner array per element +of `query.tags`, in input order. An empty inner array means no logs matched that tag. Set +`query.includeEffects` to also receive the tx's note hashes and nullifiers. -Gets public logs that match any of the `tags` from the specified contract. For each tag, an array of matching -logs is returned. An empty array implies no logs match that tag. +The return type is the widest shape — see . **Parameters**: -1. `contractAddress` - `AztecAddress` - The contract address to search logs for. -2. `tags` - `Tag[]` - The tags to search for. -3. `page` - `number | undefined` - The page number (0-indexed) for pagination. -4. `referenceBlock` - `BlockHash | undefined` - Optional block hash used to ensure the block still exists before logs are retrieved. -This block is expected to represent the latest block to which the client has synced (called anchor block in PXE). -If specified and the block is not found, an error is thrown. This helps detect reorgs, which could result in -undefined behavior in the client's code. +1. `query` - `PublicLogsQuery` -**Returns**: `TxScopedL2Log[][]` - An array of log arrays, one per tag. Returns at most 10 logs per tag per page. If 10 logs are returned -for a tag, the caller should fetch the next page to check for more logs. +**Returns**: `LogResult[][]` **Example**: ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPublicLogsByTagsFromContract","params":["0x1234...",["0x1234..."],0,"0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPublicLogsByTags","params":["0x1234..."],"id":1}' ``` ## Contract queries -### node_getContractClass +### aztec_getContractClass Returns a registered contract class given its id. @@ -731,10 +773,10 @@ Returns a registered contract class given its id. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getContractClass","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getContractClass","params":["0x1234..."],"id":1}' ``` -### node_getContract +### aztec_getContract Returns a publicly deployed contract instance given its address. @@ -749,12 +791,12 @@ Returns a publicly deployed contract instance given its address. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getContract","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getContract","params":["0x1234..."],"id":1}' ``` ## Fee queries -### node_getCurrentMinFees +### aztec_getCurrentMinFees Method to fetch the current min fees. @@ -767,10 +809,10 @@ Method to fetch the current min fees. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getCurrentMinFees","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getCurrentMinFees","params":[],"id":1}' ``` -### node_getPredictedMinFees +### aztec_getPredictedMinFees Returns predicted min fees for the current slot and next N slots. Each entry accounts for the L1 gas oracle transition and congestion growth based on the @@ -787,10 +829,10 @@ given mana usage estimate. Defaults to target usage (steady state). ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getPredictedMinFees","params":["target"],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getPredictedMinFees","params":["target"],"id":1}' ``` -### node_getMaxPriorityFees +### aztec_getMaxPriorityFees Method to fetch the current max priority fee of txs in the mempool. @@ -803,12 +845,12 @@ Method to fetch the current max priority fee of txs in the mempool. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getMaxPriorityFees","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getMaxPriorityFees","params":[],"id":1}' ``` ## Node information -### node_isReady +### aztec_isReady Method to determine if the node is ready to accept transactions. @@ -821,10 +863,10 @@ Method to determine if the node is ready to accept transactions. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_isReady","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_isReady","params":[],"id":1}' ``` -### node_getNodeInfo +### aztec_getNodeInfo Returns the information about the server's node. Includes current Node version, compatible Noir version, L1 chain identifier, protocol version, and L1 address of the rollup contract. @@ -838,10 +880,10 @@ L1 chain identifier, protocol version, and L1 address of the rollup contract. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getNodeInfo","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getNodeInfo","params":[],"id":1}' ``` -### node_getNodeVersion +### aztec_getNodeVersion Method to fetch the version of the package. @@ -854,10 +896,10 @@ Method to fetch the version of the package. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getNodeVersion","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getNodeVersion","params":[],"id":1}' ``` -### node_getVersion +### aztec_getVersion Method to fetch the version of the rollup the node is connected to. @@ -870,10 +912,10 @@ Method to fetch the version of the rollup the node is connected to. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getVersion","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getVersion","params":[],"id":1}' ``` -### node_getChainId +### aztec_getChainId Method to fetch the chain id of the base-layer for the rollup. @@ -886,10 +928,10 @@ Method to fetch the chain id of the base-layer for the rollup. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getChainId","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getChainId","params":[],"id":1}' ``` -### node_getL1ContractAddresses +### aztec_getL1ContractAddresses Method to fetch the currently deployed l1 contract addresses. @@ -902,10 +944,10 @@ Method to fetch the currently deployed l1 contract addresses. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getL1ContractAddresses","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getL1ContractAddresses","params":[],"id":1}' ``` -### node_getProtocolContractAddresses +### aztec_getProtocolContractAddresses Method to fetch the protocol contract addresses. @@ -918,10 +960,10 @@ Method to fetch the protocol contract addresses. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getProtocolContractAddresses","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getProtocolContractAddresses","params":[],"id":1}' ``` -### node_getEncodedEnr +### aztec_getEncodedEnr Returns the ENR of this node for peer discovery, if available. @@ -934,12 +976,12 @@ Returns the ENR of this node for peer discovery, if available. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getEncodedEnr","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getEncodedEnr","params":[],"id":1}' ``` ## Validator queries -### node_getValidatorsStats +### aztec_getValidatorsStats Returns stats for validators if enabled. @@ -952,10 +994,10 @@ Returns stats for validators if enabled. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getValidatorsStats","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getValidatorsStats","params":[],"id":1}' ``` -### node_getValidatorStats +### aztec_getValidatorStats Returns stats for a single validator if enabled. @@ -972,12 +1014,72 @@ Returns stats for a single validator if enabled. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getValidatorStats","params":["0x1234...","100","100"],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getValidatorStats","params":["0x1234...","100","100"],"id":1}' +``` + +## P2P queries + +### aztec_getPeers + +Returns info for all connected, dialing, and cached peers. Only available when P2P is enabled. + +**Parameters**: + +1. `includePending` - `boolean | undefined` - If true, also include peers in the pending state. + +**Returns**: `PeerInfo[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getPeers","params":[true],"id":1}' +``` + +### aztec_getCheckpointAttestationsForSlot + +Queries the attestation pool for checkpoint attestations for the given slot. + +**Parameters**: + +1. `slot` - `SlotNumber` - The slot to query. +2. `proposalPayloadHash` - `string | undefined` - Hex-encoded keccak256 of the target proposal's signed payload hash. +When provided, only attestations whose payload hash matches are returned. +When omitted, all attestations for the slot are returned. + +**Returns**: `CheckpointAttestation[]` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getCheckpointAttestationsForSlot","params":["100","0x1234..."],"id":1}' +``` + +### aztec_getProposalsForSlot + +Returns block and checkpoint proposals retained in the attestation pool for the given slot. +Only available when P2P is enabled. + +**Parameters**: + +1. `slot` - `SlotNumber` + +**Returns**: `ProposalsForSlot` + +**Example**: + +```bash +curl -X POST http://localhost:8080 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztec_getProposalsForSlot","params":["100"],"id":1}' ``` ## Debug operations -### node_registerContractFunctionSignatures +### aztec_registerContractFunctionSignatures Registers contract function signatures for debugging purposes. @@ -992,10 +1094,10 @@ Registers contract function signatures for debugging purposes. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_registerContractFunctionSignatures","params":[["0x1234..."]],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_registerContractFunctionSignatures","params":[["0x1234..."]],"id":1}' ``` -### node_getAllowedPublicSetup +### aztec_getAllowedPublicSetup Returns the list of allowed public setup elements configured for this node. @@ -1008,12 +1110,12 @@ Returns the list of allowed public setup elements configured for this node. ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getAllowedPublicSetup","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getAllowedPublicSetup","params":[],"id":1}' ``` ## Admin API -Administrative operations are exposed on port 8880 under the `nodeAdmin_` namespace. +Administrative operations are exposed on port 8880 under the `aztecAdmin_` namespace. :::warning Security: Admin API Access For security reasons, the admin port (8880) should **not be exposed** to the host machine in Docker deployments. The examples below show both CLI and Docker methods: @@ -1033,7 +1135,7 @@ docker exec -it curl -X POST http://localhost:8880 ... Replace `` with your container name (e.g., `aztec-node`, `aztec-sequencer`, `prover-node`). ::: -### nodeAdmin_getConfig +### aztecAdmin_getConfig Retrieves the configuration of this node. @@ -1046,7 +1148,7 @@ Retrieves the configuration of this node. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getConfig","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' ``` **Example (Docker)**: @@ -1054,10 +1156,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getConfig","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' ``` -### nodeAdmin_setConfig +### aztecAdmin_setConfig Updates the configuration of this node. @@ -1072,7 +1174,7 @@ Updates the configuration of this node. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_setConfig","params":[{}],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_setConfig","params":[{}],"id":1}' ``` **Example (Docker)**: @@ -1080,10 +1182,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_setConfig","params":[{}],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_setConfig","params":[{}],"id":1}' ``` -### nodeAdmin_pauseSync +### aztecAdmin_pauseSync Pauses archiver and world state syncing. @@ -1096,7 +1198,7 @@ Pauses archiver and world state syncing. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_pauseSync","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSync","params":[],"id":1}' ``` **Example (Docker)**: @@ -1104,10 +1206,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_pauseSync","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSync","params":[],"id":1}' ``` -### nodeAdmin_resumeSync +### aztecAdmin_resumeSync Resumes archiver and world state syncing. @@ -1120,7 +1222,56 @@ Resumes archiver and world state syncing. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_resumeSync","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSync","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSync","params":[],"id":1}' +``` + +### aztecAdmin_pauseSequencer + +Pauses block production. Pending txs remain in the mempool; no new blocks will be +produced until is called. Throws if no sequencer is running. + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSequencer","params":[],"id":1}' +``` + +**Example (Docker)**: + +```bash +docker exec -it aztec-node curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_pauseSequencer","params":[],"id":1}' +``` + +### aztecAdmin_resumeSequencer + +Resumes block production previously paused via . + +**Parameters**: None + +**Returns**: `void` + +**Example (CLI)**: + +```bash +curl -X POST http://localhost:8880 \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSequencer","params":[],"id":1}' ``` **Example (Docker)**: @@ -1128,10 +1279,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_resumeSync","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_resumeSequencer","params":[],"id":1}' ``` -### nodeAdmin_rollbackTo +### aztecAdmin_rollbackTo Pauses syncing and rolls back the database to the target L2 block number. @@ -1148,7 +1299,7 @@ Pauses syncing and rolls back the database to the target L2 block number. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_rollbackTo","params":[12345,true,true],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_rollbackTo","params":[12345,true,true],"id":1}' ``` **Example (Docker)**: @@ -1156,10 +1307,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_rollbackTo","params":[12345,true,true],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_rollbackTo","params":[12345,true,true],"id":1}' ``` -### nodeAdmin_startSnapshotUpload +### aztecAdmin_startSnapshotUpload Pauses syncing, creates a backup of archiver and world-state databases, and uploads them. Returns immediately. @@ -1174,7 +1325,7 @@ Pauses syncing, creates a backup of archiver and world-state databases, and uplo ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' ``` **Example (Docker)**: @@ -1182,10 +1333,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_startSnapshotUpload","params":["0x1234..."],"id":1}' ``` -### nodeAdmin_getSlashOffenses +### aztecAdmin_getSlashOffenses Returns all offenses applicable for the given round. @@ -1200,7 +1351,7 @@ Returns all offenses applicable for the given round. ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getSlashOffenses","params":["current"],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getSlashOffenses","params":["current"],"id":1}' ``` **Example (Docker)**: @@ -1208,10 +1359,10 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getSlashOffenses","params":["current"],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getSlashOffenses","params":["current"],"id":1}' ``` -### nodeAdmin_reloadKeystore +### aztecAdmin_reloadKeystore Reloads keystore configuration from disk. @@ -1239,7 +1390,7 @@ Notes: ```bash curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_reloadKeystore","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' ``` **Example (Docker)**: @@ -1247,7 +1398,7 @@ curl -X POST http://localhost:8880 \ ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_reloadKeystore","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_reloadKeystore","params":[],"id":1}' ``` ## Next steps diff --git a/docs/docs-operate/operators/sequencer-management/governance-participation.md b/docs/docs-operate/operators/sequencer-management/governance-participation.md index f499f30f031f..15941995b59d 100644 --- a/docs/docs-operate/operators/sequencer-management/governance-participation.md +++ b/docs/docs-operate/operators/sequencer-management/governance-participation.md @@ -132,7 +132,7 @@ curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_setConfig", + "method":"aztecAdmin_setConfig", "params":[{"governanceProposerPayload":"0x1234567890abcdef1234567890abcdef12345678"}], "id":1 }' @@ -146,7 +146,7 @@ docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_setConfig", + "method":"aztecAdmin_setConfig", "params":[{"governanceProposerPayload":"0x1234567890abcdef1234567890abcdef12345678"}], "id":1 }' @@ -171,7 +171,7 @@ curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_getConfig", + "method":"aztecAdmin_getConfig", "id":1 }' ``` @@ -184,7 +184,7 @@ docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_getConfig", + "method":"aztecAdmin_getConfig", "id":1 }' ``` diff --git a/docs/docs-operate/operators/sequencer-management/slashing-configuration.md b/docs/docs-operate/operators/sequencer-management/slashing-configuration.md index 9004789eb74f..99b04a6b0eb0 100644 --- a/docs/docs-operate/operators/sequencer-management/slashing-configuration.md +++ b/docs/docs-operate/operators/sequencer-management/slashing-configuration.md @@ -186,7 +186,7 @@ curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_getConfig", + "method":"aztecAdmin_getConfig", "id":1 }' ``` @@ -198,7 +198,7 @@ docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", - "method":"nodeAdmin_getConfig", + "method":"aztecAdmin_getConfig", "id":1 }' ``` diff --git a/docs/docs-operate/operators/setup/running-a-node.md b/docs/docs-operate/operators/setup/running-a-node.md index e63da5635b37..0b1da2995c91 100644 --- a/docs/docs-operate/operators/setup/running-a-node.md +++ b/docs/docs-operate/operators/setup/running-a-node.md @@ -113,7 +113,7 @@ If you need to access admin endpoints, use `docker exec`: ```bash docker exec -it aztec-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getConfig","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' ``` ::: @@ -135,7 +135,7 @@ Check the current sync status: ```bash curl -s -X POST -H 'Content-Type: application/json' \ --d '{"jsonrpc":"2.0","method":"node_getChainTips","params":[],"id":67}' \ +-d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":67}' \ http://localhost:8080 | jq -r ".result.proven.number" ``` diff --git a/docs/docs-operate/operators/setup/running-a-prover.md b/docs/docs-operate/operators/setup/running-a-prover.md index 4dbd9a44c4aa..dec4feb8fb6d 100644 --- a/docs/docs-operate/operators/setup/running-a-prover.md +++ b/docs/docs-operate/operators/setup/running-a-prover.md @@ -207,7 +207,7 @@ If you need to access admin endpoints, use `docker exec`: ```bash docker exec -it prover-node curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getConfig","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' ``` ::: diff --git a/docs/docs-operate/operators/setup/sequencer-setup.md b/docs/docs-operate/operators/setup/sequencer-setup.md index 8b0a045e3aa5..eeee80ce0de8 100644 --- a/docs/docs-operate/operators/setup/sequencer-setup.md +++ b/docs/docs-operate/operators/setup/sequencer-setup.md @@ -428,7 +428,7 @@ If you need to access admin endpoints, use `docker exec`: ```bash docker exec -it aztec-sequencer curl -X POST http://localhost:8880 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"nodeAdmin_getConfig","params":[],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztecAdmin_getConfig","params":[],"id":1}' ``` ::: @@ -452,7 +452,7 @@ Check the current sync status (this may take a few minutes): ```bash curl -s -X POST -H 'Content-Type: application/json' \ --d '{"jsonrpc":"2.0","method":"node_getChainTips","params":[],"id":67}' \ +-d '{"jsonrpc":"2.0","method":"aztec_getChainTips","params":[],"id":67}' \ http://localhost:8080 | jq -r ".result.proven.number" ``` diff --git a/docs/docs-operate/operators/setup/syncing-best-practices.md b/docs/docs-operate/operators/setup/syncing-best-practices.md index 3931e1cb4f03..88bc4341722e 100644 --- a/docs/docs-operate/operators/setup/syncing-best-practices.md +++ b/docs/docs-operate/operators/setup/syncing-best-practices.md @@ -110,7 +110,7 @@ environment: ## Creating and uploading snapshots -You can create snapshots of your node's state for backup purposes or to share with other nodes. This is done by calling the `nodeAdmin_startSnapshotUpload` method on the node admin API. +You can create snapshots of your node's state for backup purposes or to share with other nodes. This is done by calling the `aztecAdmin_startSnapshotUpload` method on the node admin API. ### How snapshot upload works @@ -132,7 +132,7 @@ Use the node admin API to trigger a snapshot upload. You can upload to Google Cl docker exec -it aztec-node curl -XPOST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ - "method": "nodeAdmin_startSnapshotUpload", + "method": "aztecAdmin_startSnapshotUpload", "params": ["gs://your-bucket/snapshots/"], "id": 1, "jsonrpc": "2.0" @@ -144,7 +144,7 @@ docker exec -it aztec-node curl -XPOST http://localhost:8880 \ docker exec -it aztec-node curl -XPOST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ - "method": "nodeAdmin_startSnapshotUpload", + "method": "aztecAdmin_startSnapshotUpload", "params": ["s3://your-bucket/snapshots/"], "id": 1, "jsonrpc": "2.0" @@ -156,7 +156,7 @@ docker exec -it aztec-node curl -XPOST http://localhost:8880 \ docker exec -it aztec-node curl -XPOST http://localhost:8880 \ -H 'Content-Type: application/json' \ -d '{ - "method": "nodeAdmin_startSnapshotUpload", + "method": "aztecAdmin_startSnapshotUpload", "params": ["s3://your-bucket/snapshots/?endpoint=https://[ACCOUNT_ID].r2.cloudflarestorage.com"], "id": 1, "jsonrpc": "2.0" @@ -209,7 +209,7 @@ To verify your sync configuration is working: ### Snapshot upload fails -**Issue**: The `nodeAdmin_startSnapshotUpload` command returns an error. +**Issue**: The `aztecAdmin_startSnapshotUpload` command returns an error. **Solutions**: - Verify storage credentials are properly configured (Google Cloud, AWS, or Cloudflare R2) diff --git a/docs/examples/ts/aztecjs_advanced/index.ts b/docs/examples/ts/aztecjs_advanced/index.ts index fbcdcdb029db..6fa75ee0bf44 100644 --- a/docs/examples/ts/aztecjs_advanced/index.ts +++ b/docs/examples/ts/aztecjs_advanced/index.ts @@ -13,7 +13,7 @@ import { SponsoredFeePaymentMethod } from "@aztec/aztec.js/fee/testing"; import { getContractInstanceFromInstantiationParams } from "@aztec/stdlib/contract"; import { PublicKeys } from "@aztec/stdlib/keys"; import { getPublicEvents } from "@aztec/aztec.js/events"; -import { GasSettings } from "@aztec/stdlib/gas"; +import { Gas, GasSettings } from "@aztec/stdlib/gas"; // Setup: connect to network const node = createAztecNodeClient( @@ -357,8 +357,9 @@ const metaResult = await token.methods .balance_of_public(aliceAddress) .simulate({ from: aliceAddress, includeMetadata: true }); console.log("Balance:", metaResult.result); -console.log("L2 gas limit:", metaResult.estimatedGas.gasLimits.l2Gas); -console.log("DA gas limit:", metaResult.estimatedGas.gasLimits.daGas); +// `gasUsed` is the raw gas the simulation consumed; derive your own limits from it (see below). +console.log("L2 gas used:", metaResult.gasUsed!.totalGas.l2Gas); +console.log("DA gas used:", metaResult.gasUsed!.totalGas.daGas); // docs:end:simulate_with_metadata // docs:start:read_public_logs @@ -375,17 +376,16 @@ if (publicLogs.length > 0) { // docs:end:read_public_logs // docs:start:estimate_mana -const { estimatedGas } = await token.methods +const { gasUsed } = await token.methods .transfer_in_public(aliceAddress, bobAddress, 1n, 0n) - .simulate({ - from: aliceAddress, - fee: { estimateGas: true, estimatedGasPadding: 0.1 }, - }); + .simulate({ from: aliceAddress, includeMetadata: true }); +// Pad the raw usage yourself to leave headroom for variance, e.g. 10%. +const estimatedGasLimits = gasUsed!.totalGas.mul(1.1); // docs:end:estimate_mana // docs:start:compute_fee_from_estimate const currentFees = await node.getCurrentMinFees(); -const estimatedFee = estimatedGas.gasLimits.computeFee(currentFees).toBigInt(); +const estimatedFee = estimatedGasLimits.computeFee(currentFees).toBigInt(); console.log("Estimated fee:", estimatedFee); // docs:end:compute_fee_from_estimate @@ -416,9 +416,17 @@ console.log("Transaction fee:", feeJuiceReceipt.transactionFee); // docs:start:custom_gas_settings // Query current network fees to set realistic limits const networkFees = await node.getCurrentMinFees(); +// Declare at most what the network admits per tx; these limits vary by network geometry, +// so read them from the node rather than hardcoding values that may exceed a given network's maximum. +const { txsLimits } = await node.getNodeInfo(); +const gasLimits = Gas.from(txsLimits.gas); const gasSettings = GasSettings.from({ - gasLimits: { daGas: 100_000, l2Gas: 2_000_000 }, - teardownGasLimits: { daGas: 100_000, l2Gas: 2_000_000 }, + gasLimits, + // Teardown must be strictly less than the total limits so app logic has gas to run. + teardownGasLimits: { + daGas: Math.floor(gasLimits.daGas / 2), + l2Gas: Math.floor(gasLimits.l2Gas / 8), + }, maxFeesPerGas: { feePerDaGas: networkFees.feePerDaGas * 2n, feePerL2Gas: networkFees.feePerL2Gas * 2n, @@ -464,17 +472,12 @@ const rangeLogs = ( // docs:end:read_logs_by_filter // docs:start:auto_gas_estimation -// Estimate gas for a transaction before sending -const { estimatedGas: autoEstimate } = await token.methods +// Read the gas a transaction would consume before sending, and pad it yourself. +const { gasUsed: autoGasUsed } = await token.methods .mint_to_public(aliceAddress, 1n) - .simulate({ - from: aliceAddress, - fee: { - estimateGas: true, - estimatedGasPadding: 0.2, // 20% padding - }, - }); -console.log("Auto-estimated L2 gas:", autoEstimate.gasLimits.l2Gas); + .simulate({ from: aliceAddress, includeMetadata: true }); +const autoEstimate = autoGasUsed!.totalGas.mul(1.2); // 20% padding +console.log("Auto-estimated L2 gas:", autoEstimate.l2Gas); // docs:end:auto_gas_estimation // docs:start:import_get_public_events diff --git a/docs/examples/ts/example_swap/index.ts b/docs/examples/ts/example_swap/index.ts index 5a81e6308b88..cb6d2c11fb0c 100644 --- a/docs/examples/ts/example_swap/index.ts +++ b/docs/examples/ts/example_swap/index.ts @@ -13,6 +13,7 @@ import { computeL2ToL1MessageHash, computeSecretHash, } from "@aztec/stdlib/hash"; +import { createAztecNodeDebugClient } from "@aztec/stdlib/interfaces/client"; import { decodeEventLog, encodeFunctionData, pad } from "@aztec/viem"; import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { foundry } from "@aztec/viem/chains"; @@ -380,6 +381,12 @@ console.log("✓ WETH transferred to bridge for swap\n"); // docs:end:public_swap // docs:start:wait_for_proof +const isLocalNetwork = + nodeUrl.includes("localhost") || + nodeUrl.includes("127.0.0.1") || + nodeUrl.includes("local-network"); +const nodeDebug = isLocalNetwork ? createAztecNodeDebugClient(nodeUrl) : undefined; + console.log("Waiting for block to be proven...\n"); let provenBlockNumber = await node.getBlockNumber("proven"); @@ -387,6 +394,9 @@ while (provenBlockNumber < swapReceipt.blockNumber!) { console.log( ` Waiting... (proven: ${provenBlockNumber}, needed: ${swapReceipt.blockNumber})`, ); + if (nodeDebug) { + await nodeDebug.mineBlock(); + } await new Promise((resolve) => setTimeout(resolve, 10000)); provenBlockNumber = await node.getBlockNumber("proven"); } @@ -445,8 +455,34 @@ const exitMsgLeaf = computeL2ToL1MessageHash({ // docs:start:consume_l1_messages_witnesses // The node picks the smallest partial-proof root that covers each tx's checkpoint. -const exitWitness = await node.getL2ToL1MembershipWitness(swapReceipt.txHash, exitMsgLeaf); -const exitSiblingPath = exitWitness!.siblingPath +const waitForL2ToL1MembershipWitness = async ( + messageName: string, + messageLeaf: Fr, +) => { + const maxAttempts = 30; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const witness = await node.getL2ToL1MembershipWitness( + swapReceipt.txHash, + messageLeaf, + ); + if (witness) { + return witness; + } + + console.log( + ` Waiting for ${messageName} L2->L1 witness (${attempt}/${maxAttempts})...`, + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + } + + throw new Error(`Timed out waiting for ${messageName} L2->L1 witness`); +}; + +const exitWitness = await waitForL2ToL1MembershipWitness( + "token bridge exit", + exitMsgLeaf, +); +const exitSiblingPath = exitWitness.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); @@ -495,8 +531,11 @@ const swapMsgLeaf = computeL2ToL1MessageHash({ chainId: new Fr(foundry.id), }); -const swapWitness = await node.getL2ToL1MembershipWitness(swapReceipt.txHash, swapMsgLeaf); -const swapSiblingPath = swapWitness!.siblingPath +const swapWitness = await waitForL2ToL1MembershipWitness( + "swap intent", + swapMsgLeaf, +); +const swapSiblingPath = swapWitness.siblingPath .toBufferArray() .map((buf: Buffer) => `0x${buf.toString("hex")}` as `0x${string}`); // docs:end:consume_l1_messages_witnesses @@ -519,12 +558,12 @@ const l1SwapHash = await l1Client.writeContract({ dir: "left", size: 32, }), - [BigInt(exitWitness!.epochNumber), BigInt(swapWitness!.epochNumber)], + [BigInt(exitWitness.epochNumber), BigInt(swapWitness.epochNumber)], [ - BigInt(exitWitness!.numCheckpointsInEpoch), - BigInt(swapWitness!.numCheckpointsInEpoch), + BigInt(exitWitness.numCheckpointsInEpoch), + BigInt(swapWitness.numCheckpointsInEpoch), ], - [BigInt(exitWitness!.leafIndex), BigInt(swapWitness!.leafIndex)], + [BigInt(exitWitness.leafIndex), BigInt(swapWitness.leafIndex)], [exitSiblingPath, swapSiblingPath], ], }); diff --git a/docs/scripts/node_api_reference_generation/generate_node_api_reference.ts b/docs/scripts/node_api_reference_generation/generate_node_api_reference.ts index 0091cbb8fda9..1b116f8dba1c 100644 --- a/docs/scripts/node_api_reference_generation/generate_node_api_reference.ts +++ b/docs/scripts/node_api_reference_generation/generate_node_api_reference.ts @@ -505,11 +505,15 @@ function simplifyZodType(expr: string): string { const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] = [ { heading: 'Block queries', - namespace: 'node', + namespace: 'aztec', methods: [ 'getBlockNumber', 'getCheckpointNumber', 'getChainTips', + 'getL1Constants', + 'getSyncedL2SlotNumber', + 'getSyncedL2EpochNumber', + 'getSyncedL1Timestamp', 'getBlock', 'getBlockData', 'getBlocks', @@ -520,7 +524,7 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] }, { heading: 'Transaction operations', - namespace: 'node', + namespace: 'aztec', methods: [ 'sendTx', 'getTxReceipt', @@ -535,12 +539,12 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] }, { heading: 'State queries', - namespace: 'node', + namespace: 'aztec', methods: ['getPublicStorageAt', 'getWorldStateSyncStatus'], }, { heading: 'Membership witnesses', - namespace: 'node', + namespace: 'aztec', methods: [ 'findLeavesIndexes', 'getNullifierMembershipWitness', @@ -552,7 +556,7 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] }, { heading: 'L1 to L2 messages', - namespace: 'node', + namespace: 'aztec', methods: [ 'getL1ToL2MessageMembershipWitness', 'getL1ToL2MessageCheckpoint', @@ -562,27 +566,22 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] }, { heading: 'Log queries', - namespace: 'node', - methods: [ - 'getPublicLogs', - 'getContractClassLogs', - 'getPrivateLogsByTags', - 'getPublicLogsByTagsFromContract', - ], + namespace: 'aztec', + methods: ['getPrivateLogsByTags', 'getPublicLogsByTags'], }, { heading: 'Contract queries', - namespace: 'node', + namespace: 'aztec', methods: ['getContractClass', 'getContract'], }, { heading: 'Fee queries', - namespace: 'node', + namespace: 'aztec', methods: ['getCurrentMinFees', 'getPredictedMinFees', 'getMaxPriorityFees'], }, { heading: 'Node information', - namespace: 'node', + namespace: 'aztec', methods: [ 'isReady', 'getNodeInfo', @@ -596,22 +595,29 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] }, { heading: 'Validator queries', - namespace: 'node', + namespace: 'aztec', methods: ['getValidatorsStats', 'getValidatorStats'], }, + { + heading: 'P2P queries', + namespace: 'aztec', + methods: ['getPeers', 'getCheckpointAttestationsForSlot', 'getProposalsForSlot'], + }, { heading: 'Debug operations', - namespace: 'node', + namespace: 'aztec', methods: ['registerContractFunctionSignatures', 'getAllowedPublicSetup'], }, { heading: 'Admin API', - namespace: 'nodeAdmin', + namespace: 'aztecAdmin', methods: [ 'getConfig', 'setConfig', 'pauseSync', 'resumeSync', + 'pauseSequencer', + 'resumeSequencer', 'rollbackTo', 'startSnapshotUpload', 'getSlashOffenses', @@ -734,7 +740,7 @@ function generateMethodMarkdown(method: MethodInfo, isAdmin: boolean): string { return lines.join('\n'); } -function generateDocument(allMethods: Map, schemaMethodNames: { node: string[]; nodeAdmin: string[] }): string { +function generateDocument(allMethods: Map, schemaMethodNames: { aztec: string[]; aztecAdmin: string[] }): string { const lines: string[] = []; // Frontmatter @@ -759,13 +765,13 @@ function generateDocument(allMethods: Map, schemaMethodNames lines.push(''); lines.push('Note that the above ports are only defaults, and can be modified by setting `--port` and `--admin-port` flags upon startup.'); lines.push(''); - lines.push('All methods use standard JSON RPC 2.0 format with methods prefixed by `node_` or `nodeAdmin_`.'); + lines.push('All methods use standard JSON RPC 2.0 format with methods prefixed by `aztec_` or `aztecAdmin_`.'); lines.push(''); const rendered = new Set(); for (const group of METHOD_GROUPS) { - const isAdmin = group.namespace === 'nodeAdmin'; + const isAdmin = group.namespace === 'aztecAdmin'; const groupMethods: MethodInfo[] = []; for (const methodName of group.methods) { @@ -782,7 +788,7 @@ function generateDocument(allMethods: Map, schemaMethodNames if (isAdmin) { lines.push('## Admin API'); lines.push(''); - lines.push('Administrative operations are exposed on port 8880 under the `nodeAdmin_` namespace.'); + lines.push('Administrative operations are exposed on port 8880 under the `aztecAdmin_` namespace.'); lines.push(''); lines.push(':::warning Security: Admin API Access'); lines.push('For security reasons, the admin port (8880) should **not be exposed** to the host machine in Docker deployments. The examples below show both CLI and Docker methods:'); @@ -815,10 +821,10 @@ function generateDocument(allMethods: Map, schemaMethodNames // Ungrouped methods const allSchemaKeys = [ - ...schemaMethodNames.node.map(m => `node:${m}`), - ...schemaMethodNames.nodeAdmin.map(m => `nodeAdmin:${m}`), + ...schemaMethodNames.aztec.map((m) => `aztec:${m}`), + ...schemaMethodNames.aztecAdmin.map((m) => `aztecAdmin:${m}`), ]; - const ungrouped = allSchemaKeys.filter(k => !rendered.has(k)); + const ungrouped = allSchemaKeys.filter((k) => !rendered.has(k)); if (ungrouped.length > 0) { lines.push('## Other methods'); lines.push(''); @@ -828,7 +834,7 @@ function generateDocument(allMethods: Map, schemaMethodNames for (const key of ungrouped) { const info = allMethods.get(key); if (info) { - const isAdmin = key.startsWith('nodeAdmin:'); + const isAdmin = key.startsWith('aztecAdmin:'); lines.push(generateMethodMarkdown(info, isAdmin)); lines.push(''); } @@ -892,13 +898,13 @@ function main() { const schema = nodeSchemaInfo.get(name)!; const jsdoc = mergedJSDoc.get(name) || { description: '', params: [], returns: '' }; if (!mergedJSDoc.has(name)) { - console.warn(`WARNING: node_${name} is missing JSDoc — rendered without description`); + console.warn(`WARNING: aztec_${name} is missing JSDoc — rendered without description`); } const paramNames = mergedParamNames.get(name) || []; - allMethods.set(`node:${name}`, { + allMethods.set(`aztec:${name}`, { name, - namespace: 'node', + namespace: 'aztec', jsdoc, paramTypes: schema.paramTypes, paramNames: paramNames.length > 0 ? paramNames : schema.paramTypes.map((_, i) => `param${i + 1}`), @@ -910,13 +916,13 @@ function main() { const schema = adminSchemaInfo.get(name)!; const jsdoc = adminInterface.jsdoc.get(name) || { description: '', params: [], returns: '' }; if (!adminInterface.jsdoc.has(name)) { - console.warn(`WARNING: nodeAdmin_${name} is missing JSDoc — rendered without description`); + console.warn(`WARNING: aztecAdmin_${name} is missing JSDoc — rendered without description`); } const paramNames = adminInterface.paramNames.get(name) || []; - allMethods.set(`nodeAdmin:${name}`, { + allMethods.set(`aztecAdmin:${name}`, { name, - namespace: 'nodeAdmin', + namespace: 'aztecAdmin', jsdoc, paramTypes: schema.paramTypes, paramNames: paramNames.length > 0 ? paramNames : schema.paramTypes.map((_, i) => `param${i + 1}`), @@ -924,7 +930,7 @@ function main() { }); } - const markdown = generateDocument(allMethods, { node: nodeMethodNames, nodeAdmin: adminMethodNames }); + const markdown = generateDocument(allMethods, { aztec: nodeMethodNames, aztecAdmin: adminMethodNames }); fs.writeFileSync(output, markdown, 'utf-8'); console.log(`Written to ${output}`); @@ -934,10 +940,10 @@ function main() { for (const m of group.methods) allGrouped.add(`${group.namespace}:${m}`); } const allKeys = [ - ...nodeMethodNames.map(m => `node:${m}`), - ...adminMethodNames.map(m => `nodeAdmin:${m}`), + ...nodeMethodNames.map((m) => `aztec:${m}`), + ...adminMethodNames.map((m) => `aztecAdmin:${m}`), ]; - const ungrouped = allKeys.filter(k => !allGrouped.has(k)); + const ungrouped = allKeys.filter((k) => !allGrouped.has(k)); if (ungrouped.length > 0) { console.warn(`\nWARNING: ${ungrouped.length} ungrouped method(s) (rendered in "Other methods"):`); for (const k of ungrouped) console.warn(` - ${k.replace(':', '_')}`); @@ -953,7 +959,7 @@ function main() { } } - console.log(`\nTotal: ${nodeMethodNames.length} node_ + ${adminMethodNames.length} nodeAdmin_ = ${nodeMethodNames.length + adminMethodNames.length} methods`); + console.log( `\nTotal: ${nodeMethodNames.length} aztec_ + ${adminMethodNames.length} aztecAdmin_ = ${nodeMethodNames.length + adminMethodNames.length} methods`); } main(); diff --git a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md index 36438de6aa8f..8b1eeb88427f 100644 --- a/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md +++ b/noir-projects/protocol-fuzzer/SANDBOX_INSTRUCTIONS.md @@ -72,7 +72,8 @@ Transaction throughput is dominated by the Aztec L2 slot duration -- each send m for the next block. Four things bring per-transaction time from ~35s down to ~4-5s: 1. **Fast slots.** The setup script starts the sandbox with 5-second L1/L2 slot durations - (default 36s/12s) and disables sequencer timetable enforcement. + (default 36s/12s). On v5 images, the local sandbox owns its local chain and uses the + automine sequencer, so it does not need a sequencer timetable override. 2. **Persistent bridge.** `wallet-bridge.mjs` keeps a single Node.js wallet instance alive inside the container. Without it, each operation would shell out to the CLI wallet, paying a ~1.5s Node.js cold-start every time. @@ -131,7 +132,6 @@ docker run -d --rm --name aztec-sandbox-nightly \ -e ETHEREUM_SLOT_DURATION=5 \ -e AZTEC_SLOT_DURATION=5 \ -e AZTEC_EPOCH_DURATION=4 \ - -e SEQ_ENFORCE_TIME_TABLE=false \ --entrypoint "" \ aztecprotocol/aztec:5.0.0-nightly.20260224 \ bash -c '/opt/foundry/bin/anvil --host 0.0.0.0 --port 8545 & \ @@ -298,7 +298,7 @@ The internal prefix wasn't stripped. Ensure `bb-avm aztec_process` ran successfu ### Wallet "inquirer not found" error Run step 3. -### "Method not found: node_getCurrentBaseFees" +### "Method not found: aztec_getCurrentBaseFees" Using the host `aztec-wallet` (`~/.aztec/bin/`) which is the `latest` version. The bridge uses the container's wallet SDK directly, so this shouldn't happen with the bridge. @@ -308,8 +308,10 @@ happens when using the old `nightly` tag (Jan 2026) with `AZTEC_SLOT_DURATION` e Use a dated nightly (`5.0.0-nightly.YYYYMMDD`) instead. ### "Block proposal initialize deadline cannot be negative" -The slot duration is too short. Even with `SEQ_ENFORCE_TIME_TABLE=false`, the sequencer -needs some minimum headroom. 5 seconds works; lower values may not. +The slot duration is too short for the sequencer timetable. 5 seconds works on older images; +lower values may not. From v5, `SEQ_ENFORCE_TIME_TABLE` is gone and this error no longer +applies to the local sandbox: the local network runs the automine sequencer, which has no +slot timetable. ## Architecture Notes diff --git a/noir-projects/protocol-fuzzer/setup-local.sh b/noir-projects/protocol-fuzzer/setup-local.sh index 8d169bf2079a..80765f261b2d 100755 --- a/noir-projects/protocol-fuzzer/setup-local.sh +++ b/noir-projects/protocol-fuzzer/setup-local.sh @@ -177,7 +177,6 @@ log "Starting Aztec node on port 8080..." ETHEREUM_SLOT_DURATION=5 \ AZTEC_SLOT_DURATION=5 \ AZTEC_EPOCH_DURATION=4 \ - SEQ_ENFORCE_TIME_TABLE=false \ LOG_LEVEL=info \ node --no-warnings ./aztec/dest/bin/index.js start \ --local-network \ diff --git a/playground/vite.config.ts b/playground/vite.config.ts index fc929320eaec..0d1ed0309156 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -144,9 +144,10 @@ export default defineConfig(({ mode }) => { // - 2026-05-08: bumped from 1750 => 1800 after merge of next into merge-train/fairies brought in barretenberg changes (optimized Poseidon2, n1 apps) that nudged bb.js over the prior limit (1750.02 KB). // - JB: bumped from 1750 => 1800 after adding the `aztec_utl_getTxEffect` oracle handler, which pulls TxEffect / FlatPublicLogs / PrivateLog / PublicDataWrite into the eager PXE import path (#22979). // - 2026-05-12: bumped from 1800 => 1850 after merge-train/barretenberg brought in further bb-side changes (multi-app kernel circuits #23076 etc.) that pushed the main entrypoint to 1801.31 KB, just over the limit raised four days earlier. + // - 2026-06-08: bumped from 1850 => 1925 after aztec RPC namespace / client surface changes pushed the main entrypoint to 1872.57 KB on CI (playground cold build). { pattern: /assets\/index-.*\.js$/, - maxSizeKB: 1850, + maxSizeKB: 1925, description: 'Main entrypoint, hard limit', }, // Bump log: diff --git a/spartan/aztec-bot/values.yaml b/spartan/aztec-bot/values.yaml index b44630c0a1c9..a7779784f0ba 100644 --- a/spartan/aztec-bot/values.yaml +++ b/spartan/aztec-bot/values.yaml @@ -56,7 +56,7 @@ bot: ELAPSED=0 while [ $ELAPSED -lt $TIMEOUT ]; do BLOCK_NUMBER=$(curl -s -X POST -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getBlockNumber","params":[],"id":1}' \ + -d '{"jsonrpc":"2.0","method":"aztec_getBlockNumber","params":[],"id":1}' \ ${AZTEC_NODE_URL} | grep -o '"result":[0-9]*' | cut -d':' -f2) if [ ! -z "$BLOCK_NUMBER" ] && [ "$BLOCK_NUMBER" -gt 0 ]; then diff --git a/spartan/aztec-snapshots/templates/cronjob.yaml b/spartan/aztec-snapshots/templates/cronjob.yaml index 1e81d89d1054..d3b6d9e94304 100644 --- a/spartan/aztec-snapshots/templates/cronjob.yaml +++ b/spartan/aztec-snapshots/templates/cronjob.yaml @@ -25,4 +25,4 @@ spec: - | set -ex echo "Starting snapshot upload to {{ .Values.snapshots.uploadLocation }} via {{ .Values.snapshots.aztecNodeAdminUrl }}" - curl -XPOST {{ .Values.snapshots.aztecNodeAdminUrl }} -d '{"method": "nodeAdmin_startSnapshotUpload", "params": ["{{ .Values.snapshots.uploadLocation }}"], "id": 1, "jsonrpc": "2.0"}' -H 'Content-Type: application/json' + curl -XPOST {{ .Values.snapshots.aztecNodeAdminUrl }} -d '{"method": "aztecAdmin_startSnapshotUpload", "params": ["{{ .Values.snapshots.uploadLocation }}"], "id": 1, "jsonrpc": "2.0"}' -H 'Content-Type: application/json' diff --git a/spartan/environments/block-capacity.env b/spartan/environments/block-capacity.env index 2893f9885d5c..af194cabfd25 100644 --- a/spartan/environments/block-capacity.env +++ b/spartan/environments/block-capacity.env @@ -51,7 +51,6 @@ DEBUG_FORCE_TX_PROOF_VERIFICATION=true SEQ_MAX_TX_PER_BLOCK=72000 # 1000 tps SEQ_MIN_TX_PER_BLOCK=0 -SEQ_ENFORCE_TIME_TABLE=true DEBUG_P2P_INSTRUMENT_MESSAGES=true LOG_LEVEL="debug; info: json-rpc, simulator" diff --git a/spartan/metrics/irm-monitor/scripts/update-monitoring.sh b/spartan/metrics/irm-monitor/scripts/update-monitoring.sh index 2f3bec4d8eaa..e1aa99decaa2 100755 --- a/spartan/metrics/irm-monitor/scripts/update-monitoring.sh +++ b/spartan/metrics/irm-monitor/scripts/update-monitoring.sh @@ -72,7 +72,7 @@ done echo "Retrieving rollup contract address..." L1_CONTRACTS=$(curl -s -X POST -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"node_getL1ContractAddresses","params":[],"id":1}' \ + -d '{"jsonrpc":"2.0","method":"aztec_getL1ContractAddresses","params":[],"id":1}' \ "http://localhost:8080" 2>&1) if [ $? -ne 0 ]; then diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index e81d90a35964..5466deabd4fd 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -127,7 +127,6 @@ SEQ_BLOCK_DURATION_MS=${SEQ_BLOCK_DURATION_MS:-} SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT=${SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT:-} SEQ_BUILD_CHECKPOINT_IF_EMPTY=${SEQ_BUILD_CHECKPOINT_IF_EMPTY:-} AZTEC_EPOCHS_LAG=${AZTEC_EPOCHS_LAG:-} -SEQ_ENFORCE_TIME_TABLE=${SEQ_ENFORCE_TIME_TABLE:-} SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT=${SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT:-0} PROVER_REPLICAS=${PROVER_REPLICAS:-4} PROVER_ENABLED=${PROVER_ENABLED:-true} @@ -582,7 +581,6 @@ SEQ_BLOCK_DURATION_MS = ${SEQ_BLOCK_DURATION_MS:-null} SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT = ${SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT:-null} SEQ_BUILD_CHECKPOINT_IF_EMPTY = ${SEQ_BUILD_CHECKPOINT_IF_EMPTY:-null} AZTEC_EPOCHS_LAG = ${AZTEC_EPOCHS_LAG:-null} -SEQ_ENFORCE_TIME_TABLE = ${SEQ_ENFORCE_TIME_TABLE:-null} SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT = ${SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT} PROVER_MNEMONIC = "${LABS_INFRA_MNEMONIC}" PROVER_PUBLISHER_MNEMONIC_START_INDEX = ${PROVER_PUBLISHER_MNEMONIC_START_INDEX} diff --git a/spartan/scripts/wait_for_l2_block.sh b/spartan/scripts/wait_for_l2_block.sh index 19f238234111..9533653c390a 100755 --- a/spartan/scripts/wait_for_l2_block.sh +++ b/spartan/scripts/wait_for_l2_block.sh @@ -35,7 +35,7 @@ echo "Waiting for L2 blocks (slot=${slot_duration}s, epoch=${epoch_duration} slo echo "Expected first block after ~${expected_wait}s from genesis, max wait ${max_wait}s from now" rpc_pod="${namespace}-rpc-aztec-node-0" -block_number_request="{\"jsonrpc\":\"2.0\",\"method\":\"node_getBlockNumber\",\"params\":[],\"id\":1}" +block_number_request="{\"jsonrpc\":\"2.0\",\"method\":\"aztec_getBlockNumber\",\"params\":[],\"id\":1}" elapsed=0 while [ $elapsed -lt $max_wait ]; do block_number=$(kubectl --request-timeout=10s exec -n "$namespace" "$rpc_pod" -- \ diff --git a/spartan/terraform/deploy-aztec-infra/main.tf b/spartan/terraform/deploy-aztec-infra/main.tf index ab7f209e8ce0..8ada41a78365 100644 --- a/spartan/terraform/deploy-aztec-infra/main.tf +++ b/spartan/terraform/deploy-aztec-infra/main.tf @@ -218,7 +218,6 @@ locals { "validator.node.env.SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT" = var.SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT "validator.node.env.SEQ_BUILD_CHECKPOINT_IF_EMPTY" = var.SEQ_BUILD_CHECKPOINT_IF_EMPTY "validator.node.env.AZTEC_EPOCHS_LAG" = var.AZTEC_EPOCHS_LAG - "validator.node.env.SEQ_ENFORCE_TIME_TABLE" = var.SEQ_ENFORCE_TIME_TABLE "validator.node.env.P2P_TX_POOL_DELETE_TXS_AFTER_REORG" = var.P2P_TX_POOL_DELETE_TXS_AFTER_REORG "validator.node.env.L1_PRIORITY_FEE_BUMP_PERCENTAGE" = var.VALIDATOR_L1_PRIORITY_FEE_BUMP_PERCENTAGE "validator.node.env.L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE" = var.VALIDATOR_L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE diff --git a/spartan/terraform/deploy-aztec-infra/variables.tf b/spartan/terraform/deploy-aztec-infra/variables.tf index b0069565e797..c3add7304d9d 100644 --- a/spartan/terraform/deploy-aztec-infra/variables.tf +++ b/spartan/terraform/deploy-aztec-infra/variables.tf @@ -390,13 +390,6 @@ variable "P2P_MAX_PENDING_TX_COUNT" { default = null } -variable "SEQ_ENFORCE_TIME_TABLE" { - description = "Whether to enforce the time table when building blocks" - type = string - nullable = true - default = null -} - variable "SEQ_SKIP_CHECKPOINT_PUBLISH_PERCENT" { description = "Percentage probability of skipping checkpoint publishing" type = string diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index cbaa9f5fe087..8ea62a41dd2d 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -163,7 +163,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra checkpointProposalSyncGrace: number; orphanPruneNoProposalTolerance: number; skipOrphanProposedBlockPruning: boolean; - blockDuration: number | undefined; + blockDuration: number; }, private readonly blobClient: BlobClientInterface, instrumentation: ArchiverInstrumentation, @@ -664,6 +664,15 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra return this.updater.removeCheckpointsAfter(checkpointNumber); } + /** + * Removes all uncheckpointed blocks strictly after `blockNumber`, along with the proposed checkpoints + * that referenced them. Used by the AutomineSequencer to undo a local insert whose propose tx failed + * to land on L1 (no reorg needed — nothing reached L1). Refuses to touch checkpointed blocks. + */ + public removeUncheckpointedBlocksAfter(blockNumber: BlockNumber): Promise { + return this.updater.removeUncheckpointedBlocksAfter(blockNumber); + } + /** Used by TXE to add checkpoints directly without syncing from L1. */ public async addCheckpoints( checkpoints: PublishedCheckpoint[], diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts index b3e61b818fd3..2521ba1e0a88 100644 --- a/yarn-project/archiver/src/factory.ts +++ b/yarn-project/archiver/src/factory.ts @@ -14,6 +14,7 @@ import { protocolContractNames } from '@aztec/protocol-contracts'; import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle'; import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi'; import type { ArchiverEmitter, BlockHash } from '@aztec/stdlib/block'; +import { DEFAULT_BLOCK_DURATION_MS } from '@aztec/stdlib/config'; import { type ContractClassPublicWithCommitment, computePublicBytecodeCommitment } from '@aztec/stdlib/contract'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { @@ -137,12 +138,10 @@ export async function createArchiver( skipHistoricalLogsCheck: false, checkpointProposalSyncGrace: config.checkpointProposalSyncGraceSeconds ?? - getDefaultCheckpointProposalSyncGrace( - config.blockDurationMs !== undefined ? config.blockDurationMs / 1000 : undefined, - ), + getDefaultCheckpointProposalSyncGrace((config.blockDurationMs ?? DEFAULT_BLOCK_DURATION_MS) / 1000), orphanPruneNoProposalTolerance: DEFAULT_ORPHAN_PRUNE_NO_PROPOSAL_TOLERANCE, skipOrphanProposedBlockPruning: false, - blockDuration: config.blockDurationMs !== undefined ? config.blockDurationMs / 1000 : undefined, + blockDuration: (config.blockDurationMs ?? DEFAULT_BLOCK_DURATION_MS) / 1000, }, mapArchiverConfig(config), ); diff --git a/yarn-project/archiver/src/modules/data_store_updater.test.ts b/yarn-project/archiver/src/modules/data_store_updater.test.ts index 6163a406e06c..73dbf6cae211 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.test.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.test.ts @@ -381,4 +381,93 @@ describe('ArchiverDataStoreUpdater', () => { addProposedBlockSpy.mockRestore(); }); }); + + describe('removeUncheckpointedBlocksAfter (automine optimistic-insert recovery)', () => { + /** Adds one proposed block plus its proposed checkpoint (one block per checkpoint, as automine does). */ + const addProposedBlockWithCheckpoint = async ( + blockNumber: number, + checkpointNumber: number, + slotNumber: number, + previousBlock?: L2Block, + ): Promise => { + const block = await L2Block.random(BlockNumber(blockNumber), { + checkpointNumber: CheckpointNumber(checkpointNumber), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + slotNumber: SlotNumber(slotNumber), + ...(previousBlock ? { lastArchive: previousBlock.archive } : {}), + }); + await updater.addProposedBlock(block); + await store.blocks.addProposedCheckpoint({ + checkpointNumber: CheckpointNumber(checkpointNumber), + header: CheckpointHeader.empty(), + startBlock: BlockNumber(blockNumber), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, + }); + return block; + }; + + it('removes the optimistic proposed block and evicts its proposed checkpoint at genesis', async () => { + const block = await addProposedBlockWithCheckpoint(1, 1, 100); + expect(await store.blocks.getBlock({ number: BlockNumber(1) })).toBeDefined(); + expect((await store.blocks.getLastProposedCheckpoint())?.checkpointNumber).toBe(1); + + const removed = await updater.removeUncheckpointedBlocksAfter(BlockNumber(0)); + + expect(removed.map(b => b.number)).toEqual([1]); + expect(removed[0].archive.root.equals(block.archive.root)).toBe(true); + expect(await store.blocks.getBlock({ number: BlockNumber(1) })).toBeUndefined(); + expect(await store.blocks.getLastProposedCheckpoint()).toBeUndefined(); + }); + + it('drops a proposed checkpoint built on the checkpointed tip without touching checkpointed state', async () => { + // Checkpointed checkpoint 1 (block 1). + const block1 = await L2Block.random(BlockNumber(1), { + checkpointNumber: CheckpointNumber(1), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + slotNumber: SlotNumber(100), + }); + await updater.addCheckpoints([makePublishedCheckpoint(makeCheckpoint([block1]), 10)]); + + // Optimistic proposed checkpoint 2 (block 2) on top. + const block2 = await addProposedBlockWithCheckpoint(2, 2, 101, block1); + expect(await store.blocks.getBlock({ number: BlockNumber(2) })).toBeDefined(); + + const removed = await updater.removeUncheckpointedBlocksAfter(BlockNumber(1)); + + expect(removed.map(b => b.number)).toEqual([2]); + expect(removed[0].archive.root.equals(block2.archive.root)).toBe(true); + expect(await store.blocks.getBlock({ number: BlockNumber(2) })).toBeUndefined(); + expect(await store.blocks.getLastProposedCheckpoint()).toBeUndefined(); + // Checkpointed checkpoint 1 and its block survive. + expect(await store.blocks.getCheckpointData(CheckpointNumber(1))).toBeDefined(); + expect(await store.blocks.getBlock({ number: BlockNumber(1) })).toBeDefined(); + }); + + it('evicts only proposed checkpoints from the pruned block onward, keeping earlier ones', async () => { + const block1 = await addProposedBlockWithCheckpoint(1, 1, 100); + await addProposedBlockWithCheckpoint(2, 2, 101, block1); + + const removed = await updater.removeUncheckpointedBlocksAfter(BlockNumber(1)); + + expect(removed.map(b => b.number)).toEqual([2]); + // Block 1 and its proposed checkpoint are untouched; only checkpoint 2 (the pruned block) is evicted. + expect(await store.blocks.getBlock({ number: BlockNumber(1) })).toBeDefined(); + expect((await store.blocks.getLastProposedCheckpoint())?.checkpointNumber).toBe(1); + }); + + it('refuses to remove checkpointed blocks', async () => { + const block1 = await L2Block.random(BlockNumber(1), { + checkpointNumber: CheckpointNumber(1), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + slotNumber: SlotNumber(100), + }); + await updater.addCheckpoints([makePublishedCheckpoint(makeCheckpoint([block1]), 10)]); + + await expect(updater.removeUncheckpointedBlocksAfter(BlockNumber(0))).rejects.toThrow( + /checkpointed blocks exist up to 1/, + ); + }); + }); }); diff --git a/yarn-project/aztec-node/src/aztec-node/config.ts b/yarn-project/aztec-node/src/aztec-node/config.ts index 9d59920335ac..6e97bb6d9773 100644 --- a/yarn-project/aztec-node/src/aztec-node/config.ts +++ b/yarn-project/aztec-node/src/aztec-node/config.ts @@ -66,6 +66,13 @@ export type AztecNodeConfig = ArchiverConfig & * See `AUTOMINE_E2E_OPTS` in `end-to-end/src/fixtures/fixtures.ts`. */ useAutomineSequencer?: boolean; + /** + * Test-only: have the AutomineSequencer automatically prove epochs (write epoch out hashes into + * the L1 Outbox and advance the proven tip) as checkpoints land, replacing the standalone + * `EpochTestSettler`. Set by the local network/sandbox; the e2e `AUTOMINE_E2E_OPTS` fixture leaves + * it off so tests drive proving manually via `prove` / `cheatCodes.rollup.markAsProven`. + */ + automineEnableProveEpoch?: boolean; }; export const aztecNodeConfigMappings: ConfigMappingsType = { @@ -109,6 +116,11 @@ export const aztecNodeConfigMappings: ConfigMappingsType = { description: 'Test-only: use AutomineSequencer instead of the production Sequencer.', ...booleanConfigHelper(false), }, + automineEnableProveEpoch: { + env: 'AUTOMINE_ENABLE_PROVE_EPOCH', + description: 'Test-only: have the AutomineSequencer automatically prove epochs as checkpoints land.', + ...booleanConfigHelper(false), + }, }; /** diff --git a/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts new file mode 100644 index 000000000000..dd71b0e627ce --- /dev/null +++ b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts @@ -0,0 +1,98 @@ +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { jsonStringify } from '@aztec/foundation/json-rpc'; +import { createSafeJsonRpcClient } from '@aztec/foundation/json-rpc/client'; +import { + type NamespacedApiHandlers, + createNamespacedSafeJsonRpcServer, + startHttpRpcServer, +} from '@aztec/foundation/json-rpc/server'; +import { + AztecNodeAdminApiSchema, + AztecNodeApiSchema, + AztecNodeDebugApiSchema, + type ChainTips, +} from '@aztec/stdlib/interfaces/client'; +import { P2PApiSchema } from '@aztec/stdlib/interfaces/server'; +import type { ApiSchemaFor } from '@aztec/stdlib/schemas'; + +import { registerAztecNodeRpcHandlers } from './register_node_rpc_handlers.js'; +import type { AztecNodeService } from './server.js'; + +type GetChainTipsOnly = { getChainTips(): Promise }; + +const GetChainTipsOnlySchema: ApiSchemaFor = { + getChainTips: AztecNodeApiSchema.getChainTips, +}; + +const p2p = {}; + +const mockNode = { + getP2P: () => p2p, + getChainTips(): Promise { + const tipId = { + block: { number: BlockNumber(1), hash: `0x01` }, + checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, + }; + return Promise.resolve({ + proposed: { number: BlockNumber(1), hash: `0x01` }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, + }); + }, +} as unknown as AztecNodeService; + +describe('registerAztecNodeRpcHandlers', () => { + it('registers aztec namespaces along with legacy aliases', () => { + const services: NamespacedApiHandlers = {}; + const adminServices: NamespacedApiHandlers = {}; + + registerAztecNodeRpcHandlers(mockNode, services, adminServices, { debug: true }); + + expect(services.aztec).toEqual([mockNode, AztecNodeApiSchema]); + expect(services.node).toBe(services.aztec); + expect(services.p2p).toEqual([p2p, P2PApiSchema]); + expect(services.aztecDebug).toEqual([mockNode, AztecNodeDebugApiSchema]); + expect(services.nodeDebug).toBe(services.aztecDebug); + expect(adminServices.aztecAdmin).toEqual([mockNode, AztecNodeAdminApiSchema]); + expect(adminServices.nodeAdmin).toBe(adminServices.aztecAdmin); + }); + + it('skips debug namespaces unless requested', () => { + const services: NamespacedApiHandlers = {}; + + registerAztecNodeRpcHandlers(mockNode, services); + + expect(services.aztecDebug).toBeUndefined(); + expect(services.nodeDebug).toBeUndefined(); + }); + + it('serves node_* methods as aliases of aztec_*', async () => { + const services: NamespacedApiHandlers = {}; + registerAztecNodeRpcHandlers(mockNode, services); + + const rpcServer = createNamespacedSafeJsonRpcServer(services); + const httpServer = await startHttpRpcServer(rpcServer, { port: 0 }); + const url = `http://127.0.0.1:${httpServer.port}`; + + const aztecClient = createSafeJsonRpcClient(url, GetChainTipsOnlySchema, { + namespaceMethods: 'aztec', + }); + const legacyClient = createSafeJsonRpcClient(url, GetChainTipsOnlySchema, { + namespaceMethods: 'node', + }); + + const expected = await aztecClient.getChainTips(); + expect(await legacyClient.getChainTips()).toEqual(expected); + + const response = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: jsonStringify({ jsonrpc: '2.0', id: 1, method: 'node_getChainTips', params: [] }), + }); + const body = (await response.json()) as { result: ChainTips }; + expect(body.result).toEqual(expected); + + httpServer.close(); + }); +}); diff --git a/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.ts b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.ts new file mode 100644 index 000000000000..d224deaa83ea --- /dev/null +++ b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.ts @@ -0,0 +1,29 @@ +import type { NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server'; +import { AztecNodeAdminApiSchema, AztecNodeApiSchema, AztecNodeDebugApiSchema } from '@aztec/stdlib/interfaces/client'; +import { P2PApiSchema } from '@aztec/stdlib/interfaces/server'; + +import type { AztecNodeService } from './server.js'; + +/** + * Registers the Aztec node RPC handlers (`aztec_*`, `aztecAdmin_*`, and optionally `aztecDebug_*`), along with the + * legacy pre-v5 namespaces (`node_*`, `nodeAdmin_*`, `nodeDebug_*`, `p2p_*`) for backwards compatibility. + */ +// TODO: Legacy support for node, nodeAdmin, nodeDebug, p2p namespaces. New namespaces introduced in v5. Remove on future release. A-1169 +export function registerAztecNodeRpcHandlers( + node: AztecNodeService, + services: NamespacedApiHandlers, + adminServices?: NamespacedApiHandlers, + options: { debug?: boolean } = {}, +): void { + services.aztec = [node, AztecNodeApiSchema]; + services.node = services.aztec; + services.p2p = [node.getP2P(), P2PApiSchema]; + if (adminServices) { + adminServices.aztecAdmin = [node, AztecNodeAdminApiSchema]; + adminServices.nodeAdmin = adminServices.aztecAdmin; + } + if (options.debug) { + services.aztecDebug = [node, AztecNodeDebugApiSchema]; + services.nodeDebug = services.aztecDebug; + } +} diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 87a563a7ab23..8d84ab641270 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -85,6 +85,17 @@ import { AztecNodeService } from './server.js'; const NOW_MS = 1718745600000; const NOW_S = NOW_MS / 1000; +// EmptyL1RollupConstants uses a 1s slot duration, which cannot fit a single block under the default 3s +// block duration the node config carries — buildProposerTimetable would derive a negative +// blocks-per-checkpoint and throw. Use a fast-profile geometry sized to one block per checkpoint (S=9, E=4) +// so the network per-tx gas admission limit equals the per-tx protocol maximum, leaving the maximal-gas mock +// txs these validation tests use admissible while still exercising the gas-limits validator at RPC. +const testL1Constants: L1RollupConstants = { + ...EmptyL1RollupConstants, + slotDuration: 9, + ethereumSlotDuration: 4, +}; + // We create a mock date provider to have control over the next slot timestamp. class MockDateProvider extends DateProvider { public override now(): number { @@ -185,7 +196,7 @@ describe('aztec node', () => { } return Promise.resolve(undefined); }) as L2BlockSource['getBlockNumber']); - l2BlockSource.getL1Constants.mockResolvedValue(EmptyL1RollupConstants); + l2BlockSource.getL1Constants.mockResolvedValue(testL1Constants); l2BlockSource.getGenesisBlockHash.mockReturnValue(BlockHash.random()); const l2LogsSource = mock(); @@ -207,10 +218,8 @@ describe('aztec node', () => { // Inject a spurious config value to test that the config is correctly picked up (nodeConfig as any).nonExistingConfig = 'foo'; - // We never request any info from the rollup contract here, since only the `getEpochAndSlotInNextL1Slot` method - // on the epoch cache is used so a simple mock will suffice. const rollupContract = mock(); - // We pass MockDateProvider to the epoch cache to have control over the next slot timestamp + // EpochCache needs a rollup object for other methods, but these tests mock `getEpochAndSlotInNextL1Slot` directly. epochCache = new EpochCache( rollupContract, { ...EmptyL1RollupConstants, lagInEpochsForValidatorSet: 0, lagInEpochsForRandao: 0 }, @@ -701,11 +710,229 @@ describe('aztec node', () => { }); describe('simulatePublicCalls', () => { + const mockNextL1Slot = (slot: SlotNumber) => { + jest.spyOn(epochCache, 'getEpochAndSlotInNextL1Slot').mockReturnValue({ + epoch: EpochNumber(0), + slot, + ts: 0n, + nowSeconds: BigInt(NOW_S), + }); + }; + + const makeSimulationBlockData = ( + blockNumber: BlockNumber, + slotNumber: SlotNumber, + checkpointNumber = CheckpointNumber(1), + ): BlockData => ({ + header: BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ blockNumber, slotNumber }), + }), + archive: L2Block.empty().archive, + blockHash: BlockHash.random(), + checkpointNumber, + indexWithinCheckpoint: IndexWithinCheckpoint(0), + }); + it('refuses to simulate public calls if the gas limit is too high', async () => { const tx = await mockTxForRollup(0x10000); unfreeze(tx.data.constants.txContext.gasSettings.gasLimits).l2Gas = 1e12; await expect(node.simulatePublicCalls(tx)).rejects.toThrow(/gas/i); }); + + it('uses the slot after the proposed checkpoint when it is later than the next L1 timestamp slot', async () => { + const tx = await mockTxForRollup(0x10000); + const checkpointNumber = CheckpointNumber(1); + const proposedCheckpointBlockNumber = BlockNumber(9); + const targetSlot = SlotNumber(10); + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ + proposed: proposedCheckpointBlockNumber, + proposedCheckpoint: checkpointNumber, + proposedCheckpointBlock: proposedCheckpointBlockNumber, + }), + ); + l2BlockSource.getBlockData.mockResolvedValue( + makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), + ); + mockNextL1Slot(SlotNumber(5)); + globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ + chainId, + version: rollupVersion, + slotNumber: targetSlot, + timestamp: 0n, + coinbase: EthAddress.ZERO, + feeRecipient: AztecAddress.ZERO, + gasFees: GasFees.empty(), + }); + + await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); + + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); + expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( + EthAddress.ZERO, + AztecAddress.ZERO, + targetSlot, + ); + }); + + it('uses the next L1 timestamp slot when it is later than the slot after the proposed checkpoint', async () => { + const tx = await mockTxForRollup(0x10000); + const checkpointNumber = CheckpointNumber(1); + const proposedCheckpointBlockNumber = BlockNumber(9); + const targetSlot = SlotNumber(12); + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ + proposed: proposedCheckpointBlockNumber, + proposedCheckpoint: checkpointNumber, + proposedCheckpointBlock: proposedCheckpointBlockNumber, + }), + ); + l2BlockSource.getBlockData.mockResolvedValue( + makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), + ); + mockNextL1Slot(targetSlot); + globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ + chainId, + version: rollupVersion, + slotNumber: targetSlot, + timestamp: 0n, + coinbase: EthAddress.ZERO, + feeRecipient: AztecAddress.ZERO, + gasFees: GasFees.empty(), + }); + + await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); + + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); + expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( + EthAddress.ZERO, + AztecAddress.ZERO, + targetSlot, + ); + }); + + it('uses the latest proposed block slot when it is ahead of the proposed checkpoint', async () => { + const tx = await mockTxForRollup(0x10000); + const checkpointNumber = CheckpointNumber(1); + const proposedCheckpointBlockNumber = BlockNumber(9); + const latestProposedBlockNumber = BlockNumber(12); + const targetSlot = SlotNumber(12); + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ + proposed: latestProposedBlockNumber, + proposedCheckpoint: checkpointNumber, + proposedCheckpointBlock: proposedCheckpointBlockNumber, + }), + ); + l2BlockSource.getBlockData.mockImplementation(query => { + if (!('number' in query)) { + return Promise.resolve(undefined); + } + if (query.number === proposedCheckpointBlockNumber) { + return Promise.resolve( + makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), + ); + } + return Promise.resolve(makeSimulationBlockData(latestProposedBlockNumber, targetSlot, checkpointNumber)); + }); + mockNextL1Slot(SlotNumber(5)); + globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ + chainId, + version: rollupVersion, + slotNumber: targetSlot, + timestamp: 0n, + coinbase: EthAddress.ZERO, + feeRecipient: AztecAddress.ZERO, + gasFees: GasFees.empty(), + }); + + await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); + + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: latestProposedBlockNumber }); + expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); + expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( + EthAddress.ZERO, + AztecAddress.ZERO, + targetSlot, + ); + }); + + it('disregards missing proposed block slots and uses the next L1 timestamp slot', async () => { + const tx = await mockTxForRollup(0x10000); + const checkpointNumber = CheckpointNumber(1); + const proposedCheckpointBlockNumber = BlockNumber(9); + const latestProposedBlockNumber = BlockNumber(12); + const targetSlot = SlotNumber(13); + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ + proposed: latestProposedBlockNumber, + proposedCheckpoint: checkpointNumber, + proposedCheckpointBlock: proposedCheckpointBlockNumber, + }), + ); + l2BlockSource.getBlockData.mockResolvedValue(undefined); + mockNextL1Slot(targetSlot); + globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ + chainId, + version: rollupVersion, + slotNumber: targetSlot, + timestamp: 0n, + coinbase: EthAddress.ZERO, + feeRecipient: AztecAddress.ZERO, + gasFees: GasFees.empty(), + }); + + await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); + + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: latestProposedBlockNumber }); + expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); + expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( + EthAddress.ZERO, + AztecAddress.ZERO, + targetSlot, + ); + }); + + it('treats slot zero as a valid proposed checkpoint slot', async () => { + const tx = await mockTxForRollup(0x10000); + const checkpointNumber = CheckpointNumber(0); + const proposedCheckpointBlockNumber = BlockNumber(0); + const targetSlot = SlotNumber(1); + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ + proposed: proposedCheckpointBlockNumber, + proposedCheckpoint: checkpointNumber, + proposedCheckpointBlock: proposedCheckpointBlockNumber, + }), + ); + l2BlockSource.getBlockData.mockResolvedValue( + makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(0), checkpointNumber), + ); + mockNextL1Slot(SlotNumber(0)); + globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ + chainId, + version: rollupVersion, + slotNumber: targetSlot, + timestamp: 0n, + coinbase: EthAddress.ZERO, + feeRecipient: AztecAddress.ZERO, + gasFees: GasFees.empty(), + }); + + await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); + + expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); + expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( + EthAddress.ZERO, + AztecAddress.ZERO, + targetSlot, + ); + }); }); describe('reloadKeystore', () => { @@ -1177,17 +1404,22 @@ describe('aztec node', () => { /** Builds an L2Tips stub with the given checkpoint numbers per tip. */ function makeTips(args: { + proposed?: BlockNumber; + proposedCheckpointBlock?: BlockNumber; proposedCheckpoint?: CheckpointNumber; checkpointed?: CheckpointNumber; proven?: CheckpointNumber; finalized?: CheckpointNumber; }): L2Tips { - const emptyBlockId = { number: BlockNumber(0), hash: '' }; - const makeTipId = (n: CheckpointNumber) => ({ block: emptyBlockId, checkpoint: { number: n, hash: '' } }); + const makeBlockId = (number = BlockNumber(0)) => ({ number, hash: '' }); + const makeTipId = (n: CheckpointNumber, blockNumber?: BlockNumber) => ({ + block: makeBlockId(blockNumber), + checkpoint: { number: n, hash: '' }, + }); return { - proposed: emptyBlockId, + proposed: makeBlockId(args.proposed), checkpointed: makeTipId(args.checkpointed ?? CheckpointNumber(0)), - proposedCheckpoint: makeTipId(args.proposedCheckpoint ?? CheckpointNumber(0)), + proposedCheckpoint: makeTipId(args.proposedCheckpoint ?? CheckpointNumber(0), args.proposedCheckpointBlock), proven: makeTipId(args.proven ?? CheckpointNumber(0)), finalized: makeTipId(args.finalized ?? CheckpointNumber(0)), }; diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index edcd656c47c8..21cdf1101744 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -10,7 +10,13 @@ import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import { type L1ContractAddresses, pickL1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils'; -import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { + BlockNumber, + CheckpointNumber, + type CheckpointProposalHash, + EpochNumber, + SlotNumber, +} from '@aztec/foundation/branded-types'; import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -35,13 +41,12 @@ import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { type ProverNode, type ProverNodeDeps, createProverNode } from '@aztec/prover-node'; import { createKeyStoreForProver } from '@aztec/prover-node/config'; import { - AutomineSequencer, FeeProviderImpl, GlobalVariableBuilder, SequencerClient, type SequencerPublisher, - createAutomineSequencer, } from '@aztec/sequencer-client'; +import { AutomineSequencer, createAutomineSequencer } from '@aztec/sequencer-client/automine'; import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; import { AttestationsBlockWatcher, @@ -83,7 +88,7 @@ import type { ProtocolContractAddresses, } from '@aztec/stdlib/contract'; import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; -import { GasFees, type ManaUsageEstimate } from '@aztec/stdlib/gas'; +import { GasFees, type ManaUsageEstimate, getNetworkTxGasLimits } from '@aztec/stdlib/gas'; import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; import type { AztecNode, @@ -99,6 +104,8 @@ import type { CheckpointParameter, CheckpointResponse, GetTxByHashOptions, + PeerInfo, + ProposalsForSlot, } from '@aztec/stdlib/interfaces/client'; import { AztecNodeAdminConfigSchema } from '@aztec/stdlib/interfaces/client'; import { @@ -118,6 +125,7 @@ import { type L2ToL1MembershipWitness, appendL1ToL2MessagesToTree, } from '@aztec/stdlib/messaging'; +import type { CheckpointAttestation } from '@aztec/stdlib/p2p'; import type { Offense } from '@aztec/stdlib/slashing'; import { DEFAULT_MIN_BLOCK_DURATION } from '@aztec/stdlib/timetable'; import type { NullifierLeafPreimage, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees'; @@ -127,6 +135,7 @@ import { type FeeProvider, type GetTxReceiptOptions, type GlobalVariableBuilder as GlobalVariableBuilderInterface, + GlobalVariables, type IndexedTxEffect, MinedTxReceipt, type MinedTxStatus, @@ -246,6 +255,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return { proposed, checkpointed, proven, finalized }; } + public getL1Constants() { + return this.blockSource.getL1Constants(); + } + + public getSyncedL2SlotNumber() { + return this.blockSource.getSyncedL2SlotNumber(); + } + + public getSyncedL2EpochNumber() { + return this.blockSource.getSyncedL2EpochNumber(); + } + + public getSyncedL1Timestamp() { + return this.blockSource.getL1Timestamp(); + } + public getCheckpointsData(query: CheckpointsQuery) { return this.blockSource.getCheckpointsData(query); } @@ -923,6 +948,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb ethereumSlotDuration: config.ethereumSlotDuration, rollupManaLimit, }, + autoSettle: config.automineEnableProveEpoch, log, }); } else { @@ -1082,14 +1108,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb } public async getNodeInfo(): Promise { - const [nodeVersion, rollupVersion, chainId, enr, contractAddresses, protocolContractAddresses] = await Promise.all([ - this.getNodeVersion(), - this.getVersion(), - this.getChainId(), - this.getEncodedEnr(), - this.getL1ContractAddresses(), - this.getProtocolContractAddresses(), - ]); + const [nodeVersion, rollupVersion, chainId, enr, contractAddresses, protocolContractAddresses, l1Constants] = + await Promise.all([ + this.getNodeVersion(), + this.getVersion(), + this.getChainId(), + this.getEncodedEnr(), + this.getL1ContractAddresses(), + this.getProtocolContractAddresses(), + this.blockSource.getL1Constants(), + ]); + + // Gas limits a single tx may declare on this network, derived from network-wide constants only (the + // timetable's blocks-per-checkpoint and the network-minimum per-block multipliers) — never this node's + // local caps or configured multipliers, which can make the node stricter at block-building time but + // cannot define what the network accepts for relay. Clients read txsLimits to set fallback gas limits. + const maxTxGas = getNetworkTxGasLimits(this.config, l1Constants); const nodeInfo: NodeInfo = { nodeVersion, @@ -1099,6 +1133,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb l1ContractAddresses: contractAddresses, protocolContractAddresses: protocolContractAddresses, realProofs: !!this.config.realProofs, + txsLimits: { gas: { daGas: maxTxGas.daGas, l2Gas: maxTxGas.l2Gas } }, }; return nodeInfo; @@ -1312,6 +1347,21 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return this.p2pClient!.getPendingTxCount(); } + public getPeers(includePending?: boolean): Promise { + return this.p2pClient!.getPeers(includePending); + } + + public getCheckpointAttestationsForSlot( + slot: SlotNumber, + proposalPayloadHash?: CheckpointProposalHash, + ): Promise { + return this.p2pClient!.getCheckpointAttestationsForSlot(slot, proposalPayloadHash); + } + + public getProposalsForSlot(slot: SlotNumber): Promise { + return this.p2pClient!.getProposalsForSlot(slot); + } + /** * Method to retrieve a single tx from the mempool or unfinalized chain. The tx's proof is only loaded and returned * when `includeProof` is set. @@ -1591,11 +1641,34 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const coinbase = EthAddress.ZERO; const feeRecipient = AztecAddress.ZERO; - const newGlobalVariables = await this.globalVariableBuilder.buildGlobalVariables( - blockNumber, + // Define the slot for simulation as the max of the next L1 timestamp slot, the slot after the proposed + // checkpoint, and the latest proposed block's slot. + const proposedCheckpointBlockData = await this.blockSource.getBlockData({ + number: l2Tips.proposedCheckpoint.block.number, + }); + const proposedCheckpointSlot = proposedCheckpointBlockData?.header.getSlot(); + let slotAfterProposedCheckpoint: SlotNumber | undefined; + if (proposedCheckpointSlot !== undefined) { + slotAfterProposedCheckpoint = SlotNumber.fromBigInt(BigInt(proposedCheckpointSlot) + 1n); + } + + let latestProposedBlockSlot: SlotNumber | undefined; + if (l2Tips.proposed.number > l2Tips.proposedCheckpoint.block.number) { + latestProposedBlockSlot = ( + await this.blockSource.getBlockData({ number: l2Tips.proposed.number }) + )?.header.getSlot(); + } + const slotFromNextL1Timestamp = this.epochCache.getEpochAndSlotInNextL1Slot().slot; + const targetSlot = SlotNumber( + Math.max(...compactArray([slotFromNextL1Timestamp, slotAfterProposedCheckpoint, latestProposedBlockSlot])), + ); + + const checkpointGlobalVariables = await this.globalVariableBuilder.buildCheckpointGlobalVariables( coinbase, feeRecipient, + targetSlot, ); + const newGlobalVariables = GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }); const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, @@ -1696,6 +1769,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot(); const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1); const l1Constants = await this.blockSource.getL1Constants(); + // Enforce the same network admission limit the node advertises in getNodeInfo (network-wide, not this + // node's local caps), so a tx the wallet sized against txsLimits is not rejected here. + const networkTxGasLimits = getNetworkTxGasLimits(this.config, l1Constants); const validator = createTxValidatorForAcceptingTxsOverRPC( db, this.contractDataSource, @@ -1711,10 +1787,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb ], gasFees: await this.getCurrentMinFees(), skipFeeEnforcement, + isSimulation, txsPermitted: !this.config.disableTransactions, - rollupManaLimit: l1Constants.rollupManaLimit, - maxBlockL2Gas: this.config.validateMaxL2BlockGas, - maxBlockDAGas: this.config.validateMaxDABlockGas, + maxTxL2Gas: networkTxGasLimits.l2Gas, + maxTxDAGas: networkTxGasLimits.daGas, }, this.log.getBindings(), ); @@ -2058,6 +2134,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb } } + public async prove(upToCheckpoint?: CheckpointNumber): Promise { + if (!this.automineSequencer) { + throw new BadRequestError('Cannot prove checkpoint: no automine sequencer is running'); + } + return await this.automineSequencer.prove(upToCheckpoint); + } + /** * Returns an instance of MerkleTreeOperations having first ensured the world state is fully synched * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data. diff --git a/yarn-project/aztec-node/src/bin/index.ts b/yarn-project/aztec-node/src/bin/index.ts index 022925cbab29..3664d69cc76c 100644 --- a/yarn-project/aztec-node/src/bin/index.ts +++ b/yarn-project/aztec-node/src/bin/index.ts @@ -1,11 +1,13 @@ #!/usr/bin/env -S node --no-warnings +import { + type NamespacedApiHandlers, + createNamespacedSafeJsonRpcServer, + startHttpRpcServer, +} from '@aztec/foundation/json-rpc/server'; import { createLogger } from '@aztec/foundation/log'; -import { AztecNodeApiSchema } from '@aztec/stdlib/interfaces/client'; -import { createTracedJsonRpcServer } from '@aztec/telemetry-client'; +import { getOtelJsonRpcPropagationMiddleware } from '@aztec/telemetry-client'; -import http from 'http'; - -import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '../index.js'; +import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars, registerAztecNodeRpcHandlers } from '../index.js'; const { AZTEC_NODE_PORT = 8081, API_PREFIX = '' } = process.env; @@ -39,12 +41,12 @@ async function main() { // eslint-disable-next-line @typescript-eslint/no-misused-promises process.once('SIGTERM', shutdown); - const rpcServer = createTracedJsonRpcServer(aztecNode, AztecNodeApiSchema); - const app = rpcServer.getApp(API_PREFIX); - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const httpServer = http.createServer(app.callback()); - httpServer.listen(+AZTEC_NODE_PORT); + const services: NamespacedApiHandlers = {}; + registerAztecNodeRpcHandlers(aztecNode, services); + const rpcServer = createNamespacedSafeJsonRpcServer(services, { + middlewares: [getOtelJsonRpcPropagationMiddleware()], + }); + await startHttpRpcServer(rpcServer, { port: +AZTEC_NODE_PORT, apiPrefix: API_PREFIX }); logger.info(`Aztec Node JSON-RPC Server listening on port ${AZTEC_NODE_PORT}`); } diff --git a/yarn-project/aztec-node/src/index.ts b/yarn-project/aztec-node/src/index.ts index 30446306da6c..701ac33511b8 100644 --- a/yarn-project/aztec-node/src/index.ts +++ b/yarn-project/aztec-node/src/index.ts @@ -1,2 +1,3 @@ export * from './aztec-node/config.js'; +export * from './aztec-node/register_node_rpc_handlers.js'; export * from './aztec-node/server.js'; diff --git a/yarn-project/aztec.js/src/api/contract.ts b/yarn-project/aztec.js/src/api/contract.ts index 20b4a353534b..a18eced0eab1 100644 --- a/yarn-project/aztec.js/src/api/contract.ts +++ b/yarn-project/aztec.js/src/api/contract.ts @@ -74,7 +74,6 @@ export { UniversalDeployMethod, } from '../contract/deploy_method.js'; export { waitForProven, type WaitForProvenOpts, DefaultWaitForProvenOpts } from '../contract/wait_for_proven.js'; -export { getGasLimits } from '../contract/get_gas_limits.js'; export { fastForwardContractUpdate } from '../contract/fastforward_contract_update.js'; export { diff --git a/yarn-project/aztec.js/src/api/wallet.ts b/yarn-project/aztec.js/src/api/wallet.ts index 62bbb6f01696..47e71e732f34 100644 --- a/yarn-project/aztec.js/src/api/wallet.ts +++ b/yarn-project/aztec.js/src/api/wallet.ts @@ -21,7 +21,6 @@ export { WalletCapabilitiesSchema, ExecutionPayloadSchema, GasSettingsOptionSchema, - WalletSimulationFeeOptionSchema, WaitOptsSchema, SendOptionsSchema, SimulateOptionsSchema, diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index a15e495f53fe..8a304c4d4dc7 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -4,7 +4,6 @@ import { ExecutionPayload, HashedValues, UtilityExecutionResult, mergeExecutionP import type { TxSimulationResultWithAppOffset } from '../wallet/tx_simulation_result_with_app_offset.js'; import type { BatchedMethod, Wallet } from '../wallet/wallet.js'; import { BaseContractInteraction } from './base_contract_interaction.js'; -import { getGasLimits } from './get_gas_limits.js'; import { NO_FROM, type RequestInteractionOptions, @@ -157,14 +156,10 @@ export class BatchCall extends BaseContractInteraction { } } - if ((options.includeMetadata || options.fee?.estimateGas) && simulatedTx) { - const { gasLimits, teardownGasLimits } = getGasLimits(simulatedTx, options.fee?.estimatedGasPadding); - this.log.verbose( - `Estimated gas limits for batch tx: DA=${gasLimits.daGas} L2=${gasLimits.l2Gas} teardownDA=${teardownGasLimits.daGas} teardownL2=${teardownGasLimits.l2Gas}`, - ); + if (options.includeMetadata && simulatedTx) { return { result: results, - estimatedGas: { gasLimits, teardownGasLimits }, + gasUsed: simulatedTx.gasUsed, offchainEffects: [], offchainMessages: [], }; diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 563eb58f7424..61ab4a6e4dd7 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -17,7 +17,6 @@ import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; import type { Wallet } from '../wallet/wallet.js'; import { BaseContractInteraction } from './base_contract_interaction.js'; -import { getGasLimits } from './get_gas_limits.js'; import { NO_FROM, type ProfileInteractionOptions, @@ -170,16 +169,12 @@ export class ContractFunctionInteraction extends BaseContractInteraction { simulatedTx.publicInputs.constants.anchorBlockHeader.globalVariables.timestamp, ); - if (options.includeMetadata || options.fee?.estimateGas) { - const { gasLimits, teardownGasLimits } = getGasLimits(simulatedTx, options.fee?.estimatedGasPadding); - this.log.verbose( - `Estimated gas limits for tx: DA=${gasLimits.daGas} L2=${gasLimits.l2Gas} teardownDA=${teardownGasLimits.daGas} teardownL2=${teardownGasLimits.l2Gas}`, - ); + if (options.includeMetadata) { return { stats: simulatedTx.stats, ...offchainOutput, result: returnValue, - estimatedGas: { gasLimits, teardownGasLimits }, + gasUsed: simulatedTx.gasUsed, }; } return { result: returnValue, ...offchainOutput }; diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 20fb1b43d310..c490af43b1a0 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -18,8 +18,8 @@ import type { ProfileOptions, SendOptions, SimulateOptions, Wallet } from '../wa import { BaseContractInteraction } from './base_contract_interaction.js'; import type { ContractBase } from './contract_base.js'; import { ContractFunctionInteraction } from './contract_function_interaction.js'; -import { getGasLimits } from './get_gas_limits.js'; import { + type InteractionFeeOptions, type InteractionWaitOptions, NO_FROM, NO_WAIT, @@ -28,7 +28,6 @@ import { type ProfileInteractionOptions, type RequestInteractionOptions, type SendInteractionOptionsWithoutWait, - type SimulationInteractionFeeOptions, type SimulationResult, type TxSendResultImmediate, extractOffchainOutput, @@ -191,7 +190,7 @@ export type DeployOptions = Deploy */ export type SimulateDeployOptions = Omit & { /** The fee options for the transaction. */ - fee?: SimulationInteractionFeeOptions; + fee?: InteractionFeeOptions; /** Simulate without checking for the validity of the resulting transaction, * e.g. whether it emits any existing nullifiers. */ skipTxValidation?: boolean; @@ -585,10 +584,6 @@ export abstract class DeployMethod { - let txSimulationResult: TxSimulationResult; - - beforeEach(async () => { - txSimulationResult = await mockSimulatedTx(); - - const tx = await mockTxForRollup(); - tx.data.gasUsed = Gas.from({ daGas: 100, l2Gas: 200 }); - txSimulationResult.publicInputs = tx.data; - - txSimulationResult.publicOutput!.gasUsed = { - totalGas: Gas.from({ daGas: 140, l2Gas: 280 }), - // Assume teardown gas limit of 20, 30 - billedGas: Gas.from({ daGas: 150, l2Gas: 290 }), - teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), - publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), - }; - }); - - it('returns gas limits from private gas usage only', () => { - txSimulationResult.publicOutput = undefined; - // Should be 110 and 220 but oh floating point - expect(getGasLimits(txSimulationResult)).toEqual({ - gasLimits: Gas.from({ daGas: 111, l2Gas: 221 }), - teardownGasLimits: Gas.empty(), - }); - }); - - it('returns gas limits for private and public', () => { - expect(getGasLimits(txSimulationResult)).toEqual({ - gasLimits: Gas.from({ daGas: 154, l2Gas: 308 }), - teardownGasLimits: Gas.from({ daGas: 11, l2Gas: 22 }), - }); - }); - - it('pads gas limits', () => { - expect(getGasLimits(txSimulationResult, 1)).toEqual({ - gasLimits: Gas.from({ daGas: 280, l2Gas: 560 }), - teardownGasLimits: Gas.from({ daGas: 20, l2Gas: 40 }), - }); - }); - - it('caps padded gas at the per-tx maxima instead of rejecting', () => { - // Both dimensions are exactly at their max, so the padding buffer would exceed them. - txSimulationResult.publicOutput!.gasUsed = { - totalGas: Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS }), - billedGas: Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS }), - teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), - publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), - }; - // The padded limits are clamped to the maxima; the under-cap teardown is padded normally. - expect(getGasLimits(txSimulationResult, 0.1)).toEqual({ - gasLimits: Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS }), - teardownGasLimits: Gas.from({ daGas: 11, l2Gas: 22 }), - }); - }); - - it('throws if simulated l2 gas exceeds the maximum processable gas', () => { - txSimulationResult.publicOutput!.gasUsed = { - totalGas: Gas.from({ daGas: 140, l2Gas: MAX_PROCESSABLE_L2_GAS + 1 }), - billedGas: Gas.from({ daGas: 150, l2Gas: MAX_PROCESSABLE_L2_GAS + 1 }), - teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), - publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), - }; - expect(() => getGasLimits(txSimulationResult, 0)).toThrow( - 'Transaction consumes more l2 gas than the maximum processable gas', - ); - }); - - it('throws if simulated da gas exceeds the per-tx blob limit', () => { - txSimulationResult.publicOutput!.gasUsed = { - totalGas: Gas.from({ daGas: MAX_TX_DA_GAS + 1, l2Gas: 280 }), - billedGas: Gas.from({ daGas: MAX_TX_DA_GAS + 1, l2Gas: 290 }), - teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), - publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), - }; - expect(() => getGasLimits(txSimulationResult, 0)).toThrow( - 'Transaction consumes more da gas than a single tx can post to a blob', - ); - }); -}); diff --git a/yarn-project/aztec.js/src/contract/get_gas_limits.ts b/yarn-project/aztec.js/src/contract/get_gas_limits.ts deleted file mode 100644 index ff9be3d3eaa3..000000000000 --- a/yarn-project/aztec.js/src/contract/get_gas_limits.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; -import { Gas } from '@aztec/stdlib/gas'; -import type { TxSimulationResult } from '@aztec/stdlib/tx'; - -/** Max gas limits for a single transaction. */ -const MAX_GAS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); - -/** - * Returns suggested total and teardown gas limits for a simulated tx. - * @param pad - Percentage to pad the suggested gas limits by, (as decimal, e.g., 0.10 for 10%). - */ -export function getGasLimits( - simulationResult: TxSimulationResult, - pad = 0.1, -): { - /** - * Gas limit for the tx, excluding teardown gas - */ - gasLimits: Gas; - /** - * Gas limit for the teardown phase - */ - teardownGasLimits: Gas; -} { - const { totalGas, teardownGas } = simulationResult.gasUsed; - - // The simulated usage must fit within the per-tx maxima, otherwise the tx can never be included. - if (totalGas.l2Gas > MAX_PROCESSABLE_L2_GAS) { - throw new Error('Transaction consumes more l2 gas than the maximum processable gas'); - } - if (totalGas.daGas > MAX_TX_DA_GAS) { - throw new Error('Transaction consumes more da gas than a single tx can post to a blob'); - } - - // Pad the limits by the buffer, then cap each dimension at its per-tx maximum so the buffer cannot - // push a limit past what inbound validation accepts. - return { - gasLimits: padGas(totalGas, pad, MAX_GAS), - teardownGasLimits: padGas(teardownGas, pad, MAX_GAS), - }; -} - -/** Pads each gas dimension capping it at its per-tx maximum. */ -function padGas(gas: Gas, pad: number, cap: Gas): Gas { - const padded = gas.mul(1 + pad); - return new Gas(Math.min(padded.daGas, cap.daGas), Math.min(padded.l2Gas, cap.l2Gas)); -} diff --git a/yarn-project/aztec.js/src/contract/interaction_options.ts b/yarn-project/aztec.js/src/contract/interaction_options.ts index 98583a42f52b..92d48488c1c2 100644 --- a/yarn-project/aztec.js/src/contract/interaction_options.ts +++ b/yarn-project/aztec.js/src/contract/interaction_options.ts @@ -2,7 +2,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import type { FieldsOf } from '@aztec/foundation/types'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; +import type { GasSettings, GasUsed, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { type Capsule, OFFCHAIN_MESSAGE_IDENTIFIER, @@ -17,16 +17,6 @@ import type { FeePaymentMethod } from '../fee/fee_payment_method.js'; import type { ProfileOptions, SendOptions, SimulateOptions } from '../wallet/index.js'; import type { WaitOpts } from './wait_opts.js'; -/** - * Options used to tweak the simulation and add gas estimation capabilities - */ -export type FeeEstimationOptions = { - /** Whether to modify the fee settings of the simulation with high gas limit to figure out actual gas settings. */ - estimateGas?: boolean; - /** Percentage to pad the estimated gas limits by, if empty, defaults to 0.1. Only relevant if estimateGas is set. */ - estimatedGasPadding?: number; -}; - /** * Interactions allow configuring a custom fee payment method that gets bundled with the transaction before * sending it to the wallet @@ -56,9 +46,6 @@ export type GasSettingsOption = { /** Fee options as set by a user. */ export type InteractionFeeOptions = GasSettingsOption & FeePaymentMethodOption; -/** Fee options that can be set for simulation *only* */ -export type SimulationInteractionFeeOptions = InteractionFeeOptions & FeeEstimationOptions; - /** * Represents the options to configure a request from a contract interaction. * Allows specifying additional auth witnesses and capsules to use during execution @@ -150,12 +137,12 @@ export type SendInteractionOptions */ export type SimulateInteractionOptions = Omit & { /** The fee options for the transaction. */ - fee?: SimulationInteractionFeeOptions; + fee?: InteractionFeeOptions; /** Simulate without checking for the validity of the resulting transaction, e.g. whether it emits any existing nullifiers. */ skipTxValidation?: boolean; /** Whether to ensure the fee payer is not empty and has enough balance to pay for the fee. */ skipFeeEnforcement?: boolean; - /** Whether to include metadata such as performance statistics (e.g. timing information of the different circuits and oracles) and gas estimation + /** Whether to include metadata such as performance statistics (e.g. timing information of the different circuits and oracles) and simulated gas usage * in the simulation result, in addition to the return value and offchain effects */ includeMetadata?: boolean; /** Pre-simulation overrides applied to the ephemeral fork and contract DB (publicStorage writes, contract instance overrides). */ @@ -220,15 +207,19 @@ export function extractOffchainOutput(effects: OffchainEffect[], anchorBlockTime /** * Represents the result of a simulation. * Always includes the return value and offchain output. - * When `includeMetadata` or `fee.estimateGas` is set, also includes stats and gas estimation. + * When `includeMetadata` is set, also includes stats and the simulated gas usage. */ export type SimulationResult = { /** Return value of the function */ result: any; /** Additional stats about the simulation. Present when `includeMetadata` is set. */ stats?: SimulationStats; - /** Gas estimation results. Present when `includeMetadata` or `fee.estimateGas` is set. */ - estimatedGas?: Pick; + /** + * Raw gas consumed by the simulated transaction. Present when `includeMetadata` is set. Apps that want to + * declare explicit gas limits should derive their own from this (e.g. pad `totalGas`) and pass them via the + * fee options; otherwise the wallet fills in the network's per-tx admission limits automatically. + */ + gasUsed?: GasUsed; } & OffchainOutput; /** Result of sendTx when not waiting for mining. */ @@ -293,8 +284,6 @@ export function toSimulateOptions(options: SimulateInteractionOptions): Simulate ...options.fee?.gasSettings, }, congestionEstimate: options.fee?.congestionEstimate, - estimateGas: options.fee?.estimateGas, - estimatedGasPadding: options.fee?.estimatedGasPadding, }, }; } diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index 4f7303e9692c..c73a28604506 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -38,7 +38,6 @@ import { import { z } from 'zod'; import { - type FeeEstimationOptions, type GasSettingsOption, type InteractionWaitOptions, NO_FROM, @@ -73,7 +72,7 @@ export type Aliased = { */ export type SimulateOptions = Omit & { /** The fee options */ - fee?: GasSettingsOption & FeeEstimationOptions; + fee?: GasSettingsOption; }; /** @@ -319,11 +318,6 @@ export const GasSettingsOptionSchema = z.object({ congestionEstimate: optional(z.nativeEnum(ManaUsageEstimate)), }); -export const WalletSimulationFeeOptionSchema = GasSettingsOptionSchema.extend({ - estimatedGasPadding: optional(z.number()), - estimateGas: optional(z.boolean()), -}); - export const WaitOptsSchema = z.object({ ignoreDroppedReceiptsFor: optional(z.number()), timeout: optional(z.number()), @@ -347,7 +341,7 @@ export const SimulateOptionsSchema = z.object({ from: FromSchema, authWitnesses: optional(z.array(AuthWitness.schema)), capsules: optional(z.array(Capsule.schema)), - fee: optional(WalletSimulationFeeOptionSchema), + fee: optional(GasSettingsOptionSchema), skipTxValidation: optional(z.boolean()), skipFeeEnforcement: optional(z.boolean()), includeMetadata: optional(z.boolean()), @@ -408,7 +402,7 @@ export const PrivateEventSchema: z.ZodType = zodFor>()( +export const PublicEventSchema: z.ZodType> = zodFor>()( z.object({ event: AbiDecodedSchema, metadata: z.intersection(inTxSchema(), z.object({ contractAddress: schemas.AztecAddress })), diff --git a/yarn-project/aztec/src/cli/aztec_start_action.ts b/yarn-project/aztec/src/cli/aztec_start_action.ts index 4c1de5380e58..5d494db13ce7 100644 --- a/yarn-project/aztec/src/cli/aztec_start_action.ts +++ b/yarn-project/aztec/src/cli/aztec_start_action.ts @@ -1,3 +1,4 @@ +import { registerAztecNodeRpcHandlers } from '@aztec/aztec-node'; import { getActiveNetworkName } from '@aztec/foundation/config'; import { type NamespacedApiHandlers, @@ -7,7 +8,6 @@ import { } from '@aztec/foundation/json-rpc/server'; import type { LogFn, Logger } from '@aztec/foundation/log'; import type { ChainConfig } from '@aztec/stdlib/config'; -import { AztecNodeAdminApiSchema, AztecNodeApiSchema, AztecNodeDebugApiSchema } from '@aztec/stdlib/interfaces/client'; import { getPackageVersion } from '@aztec/stdlib/update-checker'; import { getVersioningMiddleware } from '@aztec/stdlib/versioning'; import { getOtelJsonRpcDiagnosticsMiddleware, getOtelJsonRpcPropagationMiddleware } from '@aztec/telemetry-client'; @@ -36,21 +36,13 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg l1Mnemonic: localNetwork.l1Mnemonic, l1RpcUrls: options.l1RpcUrls, testAccounts: localNetwork.testAccounts, - realProofs: false, - // Setting the epoch duration to 2 by default for local network. This allows the epoch to be "proven" faster, so - // the users can consume out hash without having to wait for a long time. - // Note: We are not proving anything in the local network (realProofs == false). But in `createLocalNetwork`, - // the EpochTestSettler will set the out hash to the outbox when an epoch is complete. - aztecEpochDuration: 2, }, userLog, ); // Start Node and PXE JSON-RPC server signalHandlers.push(stop); - services.node = [node, AztecNodeApiSchema]; - adminServices.node = [node, AztecNodeAdminApiSchema]; - services.nodeDebug = [node, AztecNodeDebugApiSchema]; + registerAztecNodeRpcHandlers(node, services, adminServices, { debug: true }); } else { // Route --prover-node through startNode if (options.proverNode && !options.node) { @@ -61,9 +53,6 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg const { startNode } = await import('./cmds/start_node.js'); const networkName = getActiveNetworkName(options.network); ({ config } = await startNode(options, signalHandlers, services, adminServices, userLog, networkName)); - if (options.nodeDebug && services.node) { - services.nodeDebug = [services.node[0], AztecNodeDebugApiSchema]; - } } else if (options.bot) { const { startBot } = await import('./cmds/start_bot.js'); await startBot(options, signalHandlers, services, userLog); @@ -94,7 +83,7 @@ export async function aztecStart(options: any, userLog: LogFn, debugLogger: Logg // Start the main JSON-RPC server if (Object.entries(services).length > 0) { - if (services.node) { + if (services.aztec) { const { BarretenbergSync } = await import('@aztec/bb.js'); // JSON-RPC schema parsing may decompress compressed Chonk proofs before the node handler runs. await BarretenbergSync.initSingleton(); diff --git a/yarn-project/aztec/src/cli/cmds/start_node.ts b/yarn-project/aztec/src/cli/cmds/start_node.ts index 02a1b5bf0c2a..37acf1f6f936 100644 --- a/yarn-project/aztec/src/cli/cmds/start_node.ts +++ b/yarn-project/aztec/src/cli/cmds/start_node.ts @@ -1,4 +1,9 @@ -import { type AztecNodeConfig, aztecNodeConfigMappings, getConfigEnvVars } from '@aztec/aztec-node'; +import { + type AztecNodeConfig, + aztecNodeConfigMappings, + getConfigEnvVars, + registerAztecNodeRpcHandlers, +} from '@aztec/aztec-node'; import { Fr } from '@aztec/aztec.js/fields'; import { getL1Config } from '@aztec/cli/config'; import { getPublicClient } from '@aztec/ethereum/client'; @@ -15,8 +20,7 @@ import { proverBrokerBackoff, } from '@aztec/prover-client/broker'; import { type CliPXEOptions, type PXEConfig, allPxeConfigMappings } from '@aztec/pxe/config'; -import { AztecNodeAdminApiSchema, AztecNodeApiSchema } from '@aztec/stdlib/interfaces/client'; -import { P2PApiSchema, ProverNodeApiSchema, type ProvingJobBroker } from '@aztec/stdlib/interfaces/server'; +import { ProverNodeApiSchema, type ProvingJobBroker } from '@aztec/stdlib/interfaces/server'; import { type TelemetryClientConfig, initTelemetryClient, @@ -156,10 +160,7 @@ export async function startNode( // Create and start Aztec Node const node = await createAztecNode(nodeConfig, { telemetry, proverBroker: broker }, { genesis }); - // Add node and p2p to services list - services.node = [node, AztecNodeApiSchema]; - services.p2p = [node.getP2P(), P2PApiSchema]; - adminServices.nodeAdmin = [node, AztecNodeAdminApiSchema]; + registerAztecNodeRpcHandlers(node, services, adminServices, { debug: options.nodeDebug }); // Register prover-node services if the prover node subsystem is running const proverNode = node.getProverNode(); diff --git a/yarn-project/aztec/src/local-network/local-network.ts b/yarn-project/aztec/src/local-network/local-network.ts index 8612fbde91d0..4f0a760cda21 100644 --- a/yarn-project/aztec/src/local-network/local-network.ts +++ b/yarn-project/aztec/src/local-network/local-network.ts @@ -6,20 +6,17 @@ import { Fr } from '@aztec/aztec.js/fields'; import { createLogger } from '@aztec/aztec.js/log'; import { type BlobClientInterface, createBlobClient } from '@aztec/blob-client/client'; import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants'; -import { createEthereumChain } from '@aztec/ethereum/chain'; import { waitForPublicClient } from '@aztec/ethereum/client'; import { getL1ContractsConfigEnvVars } from '@aztec/ethereum/config'; import { NULL_KEY } from '@aztec/ethereum/constants'; import { deployAztecL1Contracts } from '@aztec/ethereum/deploy-aztec-l1-contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; -import { EthCheatCodes } from '@aztec/ethereum/test'; import { SecretValue } from '@aztec/foundation/config'; import { EthAddress } from '@aztec/foundation/eth-address'; import type { LogFn } from '@aztec/foundation/log'; import { DateProvider, TestDateProvider } from '@aztec/foundation/timer'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { protocolContractsHash } from '@aztec/protocol-contracts'; -import { SequencerState } from '@aztec/sequencer-client'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ProvingJobBroker } from '@aztec/stdlib/interfaces/server'; import { TxStatus } from '@aztec/stdlib/tx'; @@ -33,14 +30,12 @@ import { EmbeddedWallet } from '@aztec/wallets/embedded'; import { deployFundedSchnorrAccounts } from '@aztec/wallets/testing'; import { getGenesisValues } from '@aztec/world-state/testing'; -import { type Hex, createPublicClient, fallback, http as httpViemTransport } from 'viem'; +import type { Hex } from 'viem'; import { mnemonicToAccount, privateKeyToAddress } from 'viem/accounts'; import { foundry } from 'viem/chains'; import { createAccountLogs } from '../cli/util.js'; import { DefaultMnemonic } from '../mnemonic.js'; -import { AnvilTestWatcher } from '../testing/anvil_test_watcher.js'; -import { EpochTestSettler } from '../testing/epoch_test_settler.js'; import { getTokenAllowedSetupFunctions } from '../testing/token_allowed_setup.js'; import { publishStandardAuthRegistry } from './auth_registry.js'; import { getBananaFPCAddress, setupBananaFPC } from './banana_fpc.js'; @@ -54,8 +49,6 @@ const logger = createLogger('local-network'); // P2P node"). Wait for the checkpoint so each setup tx is durably included before the next is sent. const setupWaitOpts = { waitForStatus: TxStatus.CHECKPOINTED }; -const localAnvil = foundry; - /** * Function to deploy our L1 contracts to the local network L1 * @param aztecNodeConfig - The Aztec Node Config @@ -116,11 +109,29 @@ export async function createLocalNetwork(config: Partial = { // in the setup allowlist so FPC-based fee payments work out of the box. const tokenAllowList = await getTokenAllowedSetupFunctions(); + const envConfig = getConfigEnvVars(); const aztecNodeConfig: AztecNodeConfig = { - ...getConfigEnvVars(), + ...envConfig, ...config, skipOrphanProposedBlockPruning: true, txPublicSetupAllowListExtend: [...tokenAllowList, ...(config.txPublicSetupAllowListExtend ?? [])], + // The local network runs against anvil with no committee, so it defaults to the deterministic + // AutomineSequencer, which owns L1 time control (warps the dateProvider and L1 timestamps to slot + // boundaries as it builds), replacing the deleted AnvilTestWatcher. This remains true when p2p is + // enabled for local peer testing; local-network is not a mode for connecting to an existing network. + useAutomineSequencer: config.useAutomineSequencer ?? true, + // The AutomineSequencer owns epoch proving in the local network — it writes epoch out hashes to + // the L1 Outbox and advances the proven tip as checkpoints land, through the same serial queue as + // its builds — replacing the standalone EpochTestSettler that used to race the build loop. + automineEnableProveEpoch: config.automineEnableProveEpoch ?? true, + // Defaults for the local network / sandbox; callers (e.g. the CLI) may override. No real proving + // happens here — the AutomineSequencer synthetically settles epochs. Short epochs let it write out + // hashes quickly (so users can consume L2-to-L1 messages without a long wait), with a wider + // proof-submission window so the synthetic settler has headroom before the rollup would prune an + // unproven checkpoint. + realProofs: config.realProofs ?? false, + aztecEpochDuration: config.aztecEpochDuration ?? 4, + aztecProofSubmissionEpochs: config.aztecProofSubmissionEpochs ?? 2, }; const hdAccount = mnemonicToAccount(config.l1Mnemonic || DefaultMnemonic); if ( @@ -144,7 +155,7 @@ export async function createLocalNetwork(config: Partial = { const initialAccounts = await (async () => { if (config.testAccounts === true || config.testAccounts === undefined) { if (aztecNodeConfig.p2pEnabled) { - userLog(`Not setting up test accounts as we are connecting to a network`); + userLog(`Not setting up test accounts when p2p is enabled`); } else { userLog(`Setting up test accounts`); return await getInitialTestAccountsData(); @@ -165,36 +176,11 @@ export async function createLocalNetwork(config: Partial = { const dateProvider = new TestDateProvider(); - let cheatcodes: EthCheatCodes | undefined; - let rollupAddress: EthAddress | undefined; - let watcher: AnvilTestWatcher | undefined; if (!aztecNodeConfig.p2pEnabled) { - ({ rollupAddress } = await deployContractsToL1( - aztecNodeConfig, - aztecNodeConfig.validatorPrivateKeys.getValue()[0], - { - genesisArchiveRoot, - feeJuicePortalInitialBalance: fundingNeeded, - }, - )); - - const chain = - aztecNodeConfig.l1RpcUrls.length > 0 - ? createEthereumChain([l1RpcUrl], aztecNodeConfig.l1ChainId) - : { chainInfo: localAnvil }; - - const publicClient = createPublicClient({ - chain: chain.chainInfo, - transport: fallback([httpViemTransport(l1RpcUrl)]) as any, + await deployContractsToL1(aztecNodeConfig, aztecNodeConfig.validatorPrivateKeys.getValue()[0], { + genesisArchiveRoot, + feeJuicePortalInitialBalance: fundingNeeded, }); - - cheatcodes = new EthCheatCodes([l1RpcUrl], dateProvider); - - watcher = new AnvilTestWatcher(cheatcodes, rollupAddress, publicClient, dateProvider); - watcher.setisLocalNetwork(true); - watcher.setIsMarkingAsProven(false); // Do not mark as proven in the watcher. It's marked in the epochTestSettler after the out hash is set. - - await watcher.start(); } const telemetry = await initTelemetryClient(getTelemetryClientConfig()); @@ -202,48 +188,6 @@ export async function createLocalNetwork(config: Partial = { const blobClient = createBlobClient(); const node = await createAztecNode(aztecNodeConfig, { telemetry, blobClient, dateProvider }, { genesis }); - // Now that the node is up, let the watcher check for pending txs so it can skip unfilled slots faster when - // transactions are waiting in the mempool. Also let it check if the sequencer is actively building, to avoid - // warping time out from under an in-progress block. - watcher?.setGetPendingTxCount(() => node.getPendingTxCount()); - const sequencer = node.getSequencer()?.getSequencer(); - if (sequencer) { - const idleStates: Set = new Set([ - SequencerState.STOPPED, - SequencerState.STOPPING, - SequencerState.IDLE, - SequencerState.SYNCHRONIZING, - ]); - watcher?.setIsSequencerBuilding(() => !idleStates.has(sequencer.getState())); - // Under proposer pipelining the L1 publish for slot N happens during wall-clock slot N, - // but the proposer for slot N has already built the checkpoint during slot N-1 and is - // waiting for L1 to advance. We need to fast-forward L1 to wake that wait — and the wait - // we have to break first is `waitForValidParentCheckpointOnL1`, which blocks the - // checkpoint_proposal_job's background submission task until the archiver has synced past - // the build slot. That wait happens *before* `PUBLISHING_CHECKPOINT` is set, so a hook on - // that state transition would be circular (L1 has to advance before the state we'd use to - // advance L1 fires). The earliest pre-wait signal is `block-proposed`, which the sequencer - // emits once each block is built. In sandbox single-block-per-slot mode this is - // effectively "checkpoint built", and the watcher warp is harmless if a subsequent - // assembly/validation/parent-wait step aborts: L1 just sits one slot ahead, which the - // cascade absorbs. - if (watcher) { - sequencer.on('block-proposed', ({ slot }) => watcher!.setProposedTargetSlot(Number(slot))); - } - } - - let epochTestSettler: EpochTestSettler | undefined; - if (!aztecNodeConfig.p2pEnabled) { - epochTestSettler = new EpochTestSettler( - cheatcodes!, - rollupAddress!, - node.getBlockSource(), - logger.createChild('epoch-settler'), - { pollingIntervalMs: 200 }, - ); - await epochTestSettler.start(); - } - if (initialAccounts.length) { const wallet = await EmbeddedWallet.create(node, { pxeConfig: { proverEnabled: aztecNodeConfig.realProofs }, @@ -268,8 +212,6 @@ export async function createLocalNetwork(config: Partial = { const stop = async () => { await node.stop(); - await watcher?.stop(); - await epochTestSettler?.stop(); }; return { node, stop }; diff --git a/yarn-project/aztec/src/testing/anvil_test_watcher.ts b/yarn-project/aztec/src/testing/anvil_test_watcher.ts deleted file mode 100644 index 81505d5296fa..000000000000 --- a/yarn-project/aztec/src/testing/anvil_test_watcher.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; -import type { ViemClient } from '@aztec/ethereum/types'; -import { SlotNumber } from '@aztec/foundation/branded-types'; -import type { EthAddress } from '@aztec/foundation/eth-address'; -import { type Logger, createLogger } from '@aztec/foundation/log'; -import { RunningPromise } from '@aztec/foundation/running-promise'; -import type { TestDateProvider } from '@aztec/foundation/timer'; -import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi'; - -import { type GetContractReturnType, getAddress, getContract } from 'viem'; - -export type AnvilTestWatcherOpts = { - isLocalNetwork?: boolean; - isMarkingAsProven?: boolean; -}; - -/** - * Represents a watcher for a rollup contract. - * - * It started on a network like anvil where time traveling is allowed, and auto-mine is turned on - * it will periodically check if the current slot have already been filled, e.g., there was an L2 - * block within the slot. And if so, it will time travel into the next slot. - */ -export class AnvilTestWatcher { - private isLocalNetwork; - private isMarkingAsProven; - - private rollup: GetContractReturnType; - private rollupCheatCodes: RollupCheatCodes; - private l2SlotDuration!: number; - - private filledRunningPromise?: RunningPromise; - private syncDateProviderPromise?: RunningPromise; - private markingAsProvenRunningPromise?: RunningPromise; - - private logger: Logger = createLogger(`aztecjs:utils:watcher`); - - // Optional callback to check if there are pending txs in the mempool. - private getPendingTxCount?: () => Promise; - - // Optional callback to check if the sequencer is actively building a block. - private isSequencerBuilding?: () => boolean; - - // Tracks when we first observed the current unfilled slot with pending txs (real wall time). - private unfilledSlotFirstSeen?: { slot: number; realTime: number }; - - // Latest target slot for which the proposer has built a block destined for L1 but which has - // not yet been committed. Set by the proposer-pipelining hook from `block-proposed` events so - // the watcher can advance L1 (and the injected date provider) to the target slot ahead of the - // publisher's `sendRequestsAt` sleep, instead of waiting a full wall-clock slot. - private proposedTargetSlot?: number; - - constructor( - private cheatcodes: EthCheatCodes, - rollupAddress: EthAddress, - l1Client: ViemClient, - private dateProvider?: TestDateProvider, - opts: AnvilTestWatcherOpts = {}, - ) { - this.rollup = getContract({ - address: getAddress(rollupAddress.toString()), - abi: RollupAbi, - client: l1Client, - }); - - this.rollupCheatCodes = new RollupCheatCodes(this.cheatcodes, { - rollupAddress, - }); - - this.isLocalNetwork = opts.isLocalNetwork ?? false; - this.isMarkingAsProven = opts.isMarkingAsProven ?? true; - - this.logger.debug(`Watcher created for rollup at ${rollupAddress}`); - } - - setIsMarkingAsProven(isMarkingAsProven: boolean) { - this.logger.warn(`Watcher is now ${isMarkingAsProven ? 'marking' : 'not marking'} blocks as proven`); - this.isMarkingAsProven = isMarkingAsProven; - } - - setisLocalNetwork(isLocalNetwork: boolean) { - this.isLocalNetwork = isLocalNetwork; - } - - /** Sets a callback to check for pending txs, used to skip unfilled slots faster when txs are waiting. */ - setGetPendingTxCount(fn: () => Promise) { - this.getPendingTxCount = fn; - } - - /** Sets a callback to check if the sequencer is actively building, to avoid warping while it works. */ - setIsSequencerBuilding(fn: () => boolean) { - this.isSequencerBuilding = fn; - } - - /** - * Records the target slot for which the proposer has built a block destined for L1. Used by - * the local-network watcher to fast-forward L1 (and the injected date provider) ahead of the - * pipelined publisher's `sendRequestsAt` sleep so it ends promptly instead of waiting a full - * wall-clock slot. Only ratchets up — late warps for stale slots are no-ops. - */ - setProposedTargetSlot(slot: number) { - if (this.proposedTargetSlot === undefined || slot > this.proposedTargetSlot) { - this.proposedTargetSlot = slot; - } - } - - async start() { - if (this.filledRunningPromise) { - throw new Error('Watcher already watching for filled slot'); - } - - const config = await this.rollupCheatCodes.getConfig(); - this.l2SlotDuration = config.slotDuration; - - // If auto mining is not supported (e.g., we are on a real network), then we - // will simple do nothing. But if on an anvil or the like, this make sure that - // the local network and tests don't break because time is frozen and we never get to - // the next slot. - const isAutoMining = await this.cheatcodes.isAutoMining(); - - if (isAutoMining) { - this.filledRunningPromise = new RunningPromise(() => this.warpTimeIfNeeded(), this.logger, 200); - this.filledRunningPromise.start(); - this.syncDateProviderPromise = new RunningPromise(() => this.syncDateProviderToL1IfBehind(), this.logger, 200); - this.syncDateProviderPromise.start(); - this.markingAsProvenRunningPromise = new RunningPromise(() => this.markAsProven(), this.logger, 200); - this.markingAsProvenRunningPromise.start(); - this.logger.info(`Watcher started for rollup at ${this.rollup.address}`); - } else { - this.logger.info(`Watcher not started because not auto mining`); - } - } - - async stop() { - await this.filledRunningPromise?.stop(); - await this.syncDateProviderPromise?.stop(); - await this.markingAsProvenRunningPromise?.stop(); - } - - async trigger() { - await this.filledRunningPromise?.trigger(); - await this.syncDateProviderPromise?.trigger(); - await this.markingAsProvenRunningPromise?.trigger(); - } - - async markAsProven() { - if (!this.isMarkingAsProven) { - return; - } - await this.rollupCheatCodes.markAsProven(); - } - - async syncDateProviderToL1IfBehind() { - // this doesn't apply to the local network, because we don't have a date provider in the local network - if (!this.dateProvider) { - return; - } - - const l1Time = (await this.cheatcodes.lastBlockTimestamp()) * 1000; - const wallTime = this.dateProvider.now(); - if (l1Time > wallTime) { - this.logger.warn(`L1 is ahead of wall time. Syncing wall time to L1 time`); - this.dateProvider.setTime(l1Time); - } else if (l1Time + Number(this.l2SlotDuration) * 1000 < wallTime) { - // Warp L1 to the slot boundary at-or-before wall time. Rounding to a slot boundary (rather than - // `ceil(wallTime / 1000)`) keeps this loop's target aligned with `warpTimeIfNeeded`'s - // `nextSlotTimestamp` target, avoiding a race where the two loops pick timestamps a fraction of - // a second apart and one of them is then rejected by anvil as non-monotonic. - const wallSec = Math.floor(wallTime / 1000); - const targetSlot = await this.rollup.read.getSlotAt([BigInt(wallSec)]); - const targetTimestamp = Number(await this.rollup.read.getTimestampForSlot([targetSlot])); - this.logger.warn(`L1 is more than 1 L2 slot behind wall time. Warping to slot ${targetSlot} boundary`); - await this.warpToTimestamp(targetTimestamp); - } - } - - async warpTimeIfNeeded() { - try { - const currentSlot = SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot()); - const pendingCheckpointNumber = await this.rollup.read.getPendingCheckpointNumber(); - const checkpointLog = await this.rollup.read.getCheckpoint([pendingCheckpointNumber]); - const nextSlot = SlotNumber(currentSlot + 1); - const nextSlotTimestamp = Number(await this.rollup.read.getTimestampForSlot([BigInt(nextSlot)])); - - if (BigInt(currentSlot) === checkpointLog.slotNumber) { - // The current slot has been filled, we should jump to the next slot. - if (await this.warpToTimestamp(nextSlotTimestamp)) { - this.logger.info(`Slot ${currentSlot} was filled, jumped to next slot`); - } - return; - } - - // If we are not in local network, we don't need to warp time - if (!this.isLocalNetwork) { - return; - } - - // Pipelined-publish shortcut: if the proposer has built a block destined for a slot - // beyond the current L1 slot, fast-forward L1 to that slot's timestamp so the publisher's - // `sendRequestsAt(targetSlot)` sleep ends and the multicall mines inside the target slot. - // Without this, the publisher waits up to a full real-time slot for wall clock to catch up. - if (this.proposedTargetSlot !== undefined && this.proposedTargetSlot > currentSlot) { - const targetSlotTimestamp = Number( - await this.rollup.read.getTimestampForSlot([BigInt(this.proposedTargetSlot)]), - ); - if (await this.warpToTimestamp(targetSlotTimestamp)) { - this.logger.info(`Warped L1 to target slot ${this.proposedTargetSlot} for pipelined publish`); - } - return; - } - - // If there are pending txs and the sequencer missed them, warp quickly (after a 2s real-time debounce) so the - // sequencer can retry in the next slot. Without this, we'd have to wait a full real-time slot duration (~36s) for - // the dateProvider to catch up to the next slot timestamp. We skip the warp if the sequencer is actively building - // to avoid invalidating its in-progress work. - if (this.getPendingTxCount) { - const pendingTxs = await this.getPendingTxCount(); - if (pendingTxs > 0) { - if (this.isSequencerBuilding?.()) { - this.unfilledSlotFirstSeen = undefined; - return; - } - - const realNow = Date.now(); - if (!this.unfilledSlotFirstSeen || this.unfilledSlotFirstSeen.slot !== currentSlot) { - this.unfilledSlotFirstSeen = { slot: currentSlot, realTime: realNow }; - return; - } - - if (realNow - this.unfilledSlotFirstSeen.realTime > 2000) { - if (await this.warpToTimestamp(nextSlotTimestamp)) { - this.logger.info(`Slot ${currentSlot} was missed with pending txs, jumped to next slot`); - } - this.unfilledSlotFirstSeen = undefined; - } - - return; - } - } - - // Fallback: warp when the dateProvider time has passed the next slot timestamp. - const currentTimestamp = this.dateProvider?.now() ?? Date.now(); - if (currentTimestamp > nextSlotTimestamp * 1000) { - if (await this.warpToTimestamp(nextSlotTimestamp)) { - this.logger.info(`Slot ${currentSlot} was missed, jumped to next slot`); - } - } - } catch { - this.logger.error('mineIfSlotFilled failed'); - } - } - - /** - * Warps L1 to `timestamp`, unless L1 is already at or past it. Returns true when a warp actually - * happened, false when skipped or on error. Callers use the return value to gate success logs. - */ - private async warpToTimestamp(timestamp: number): Promise { - try { - // Anvil rejects evm_setNextBlockTimestamp values <= the current block's timestamp. The two - // watcher loops can race and pick targets a fraction of a second apart; skip here rather than - // letting the second one error out noisily. - const lastTimestamp = await this.cheatcodes.lastBlockTimestamp(); - if (timestamp <= lastTimestamp) { - return false; - } - await this.cheatcodes.warp(timestamp, { resetBlockInterval: true }); - return true; - } catch (e) { - this.logger.error(`Failed to warp to timestamp ${timestamp}: ${e}`); - return false; - } - } -} diff --git a/yarn-project/aztec/src/testing/cheat_codes.ts b/yarn-project/aztec/src/testing/cheat_codes.ts index 9e2f459bbe11..e1f7c86694b7 100644 --- a/yarn-project/aztec/src/testing/cheat_codes.ts +++ b/yarn-project/aztec/src/testing/cheat_codes.ts @@ -2,7 +2,7 @@ import { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; import { SlotNumber } from '@aztec/foundation/branded-types'; import { createLogger } from '@aztec/foundation/log'; import type { DateProvider } from '@aztec/foundation/timer'; -import type { AutomineSequencer } from '@aztec/sequencer-client'; +import type { AutomineSequencer } from '@aztec/sequencer-client/automine'; import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; /** @@ -64,21 +64,18 @@ export class CheatCodes { } const currentSlot = await this.rollup.getSlot(); - const targetSlot = await this.rollup.getSlotAt(targetBigInt); + let effectiveTargetSlot = await this.rollup.getSlotAt(targetBigInt); + let effectiveTimestamp = await this.rollup.getTimestampForSlot(effectiveTargetSlot); - let effectiveTimestamp = targetBigInt; - let effectiveTargetSlot = targetSlot; - - if (targetSlot <= currentSlot) { - // Target lands in the same (or earlier) slot — auto-adjust to the next slot's start. - const nextSlot = SlotNumber(currentSlot + 1); - const nextSlotTimestamp = await this.rollup.getTimestampForSlot(nextSlot); + if (effectiveTimestamp < targetBigInt || effectiveTargetSlot <= currentSlot) { + const adjustedSlot = SlotNumber(Math.max(effectiveTargetSlot + 1, currentSlot + 1)); + const adjustedTimestamp = await this.rollup.getTimestampForSlot(adjustedSlot); this.logger.warn( - `warpL2TimeAtLeastTo: target timestamp ${targetBigInt} falls in current slot ${currentSlot}. ` + - `Auto-adjusting to start of slot ${nextSlot} at timestamp ${nextSlotTimestamp}.`, + `warpL2TimeAtLeastTo: target timestamp ${targetBigInt} does not align with a future L2 slot boundary. ` + + `Auto-adjusting to start of slot ${adjustedSlot} at timestamp ${adjustedTimestamp}.`, ); - effectiveTimestamp = nextSlotTimestamp; - effectiveTargetSlot = nextSlot; + effectiveTimestamp = adjustedTimestamp; + effectiveTargetSlot = adjustedSlot; } await this.eth.warp(effectiveTimestamp, { resetBlockInterval: true }); diff --git a/yarn-project/aztec/src/testing/epoch_test_settler.ts b/yarn-project/aztec/src/testing/epoch_test_settler.ts index 50b52a1ade32..b84785a88ce5 100644 --- a/yarn-project/aztec/src/testing/epoch_test_settler.ts +++ b/yarn-project/aztec/src/testing/epoch_test_settler.ts @@ -1,10 +1,9 @@ -import { Fr } from '@aztec/aztec.js/fields'; import { type EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; -import { type EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import type { EpochNumber } from '@aztec/foundation/branded-types'; import type { Logger } from '@aztec/foundation/log'; +import { settleEpochOutbox } from '@aztec/prover-client/test'; import { EpochMonitor } from '@aztec/prover-node'; import type { EthAddress, L2BlockSource } from '@aztec/stdlib/block'; -import { computeEpochOutHash } from '@aztec/stdlib/messaging'; export class EpochTestSettler { private rollupCheatCodes: RollupCheatCodes; @@ -31,33 +30,12 @@ export class EpochTestSettler { } async handleEpochReadyToProve(epoch: EpochNumber): Promise { - const blocks = await this.l2BlockSource.getBlocks({ epoch, onlyCheckpointed: true }); - this.log.info( - `Settling epoch ${epoch} with blocks ${blocks[0]?.header.getBlockNumber()} to ${blocks.at(-1)?.header.getBlockNumber()}`, - { blocks: blocks.map(b => b.toBlockInfo()) }, - ); - const messagesInEpoch: Fr[][][][] = []; - let previousSlotNumber = SlotNumber.ZERO; - let checkpointIndex = -1; - - for (const block of blocks) { - const slotNumber = block.header.globalVariables.slotNumber; - if (slotNumber !== previousSlotNumber) { - checkpointIndex++; - messagesInEpoch[checkpointIndex] = []; - previousSlotNumber = slotNumber; - } - messagesInEpoch[checkpointIndex].push(block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)); - } - - const outHash = computeEpochOutHash(messagesInEpoch); - if (!outHash.isZero()) { - await this.rollupCheatCodes.insertOutbox(epoch, messagesInEpoch.length, outHash.toBigInt()); - } else { - this.log.info(`No L2 to L1 messages in epoch ${epoch}`); - } - - const lastCheckpoint = blocks.at(-1)?.checkpointNumber; + const lastCheckpoint = await settleEpochOutbox({ + rollupCheatCodes: this.rollupCheatCodes, + l2BlockSource: this.l2BlockSource, + epoch, + log: this.log, + }); if (lastCheckpoint !== undefined) { await this.rollupCheatCodes.markAsProven(lastCheckpoint); } else { diff --git a/yarn-project/aztec/src/testing/index.ts b/yarn-project/aztec/src/testing/index.ts index 3fa5faf6b2d2..0155a48144fd 100644 --- a/yarn-project/aztec/src/testing/index.ts +++ b/yarn-project/aztec/src/testing/index.ts @@ -1,4 +1,3 @@ -export { AnvilTestWatcher, type AnvilTestWatcherOpts } from './anvil_test_watcher.js'; export { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; export { CheatCodes } from './cheat_codes.js'; export { EpochTestSettler } from './epoch_test_settler.js'; diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts index 8c1f55d90644..b5648bcea5de 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts @@ -188,8 +188,7 @@ describe('AVM check-circuit – unhappy paths 3', () => { 'a nested exceptional halt is recovered from in caller', async () => { // The contract requires >200k DA gas (it allocates da_gas_left - 200_000 to the nested call). - // Use a higher DA gas limit than the default since APPROXIMATE_MAX_DA_GAS_PER_BLOCK is ~196k. - // For more information, refer to yarn-project/stdlib/src/gas/gas_settings.ts + // Use a higher DA gas limit than the default since the per-block DA share is ~196k at 4 blocks/checkpoint. const gasLimits = new Gas(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS); await tester.simProveVerifyAppLogic( { address: avmTestContractInstance.address, fnName: 'external_call_to_divide_by_zero_recovers', args: [] }, diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index aa04f1c75dbd..884262eb5389 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -30,7 +30,7 @@ import { PrivateTokenContract } from '@aztec/noir-contracts.js/PrivateToken'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; +import { GasFees, ManaUsageEstimate } from '@aztec/stdlib/gas'; import type { AztecNode, AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -524,13 +524,13 @@ export class BotFactory { if (useClaim && mnemonicOrPrivateKey) { const claim = await this.getOrCreateBridgeClaim(sender!); const paymentMethod = new FeeJuicePaymentMethodWithClaim(sender!, claim); - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true, paymentMethod } }); const maxFeesPerGas = (await this.getMinFees()).mul(1 + this.config.minFeePadding); - const gasSettings = GasSettings.from({ - ...estimatedGas!, + // Leave gas limits unset so the wallet derives them from its own simulation and clamps to the + // network's per-tx admission limits. + const gasSettings = { maxFeesPerGas, maxPriorityFeesPerGas: GasFees.empty(), - }); + }; await this.withNoMinTxsPerBlock(async () => { const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings, paymentMethod }, wait: NO_WAIT }); this.log.info( @@ -540,10 +540,11 @@ export class BotFactory { }); await this.store.deleteBridgeClaim(sender!); } else { - const { estimatedGas } = await deploy.simulate({ ...deployOpts, fee: { estimateGas: true } }); - this.log.info(`Deploying contract ${name} at ${address.toString()}`, { estimatedGas }); + this.log.info(`Deploying contract ${name} at ${address.toString()}`); + // Gas limits are left unset so the wallet derives them from its own simulation and clamps to the + // network's per-tx admission limits. await this.withNoMinTxsPerBlock(async () => { - const { txHash } = await deploy.send({ ...deployOpts, fee: { gasSettings: estimatedGas }, wait: NO_WAIT }); + const { txHash } = await deploy.send({ ...deployOpts, wait: NO_WAIT }); this.log.info(`Sent contract ${name} setup tx with hash ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); @@ -591,15 +592,12 @@ export class BotFactory { while (balance < FEE_JUICE_TOP_UP_TARGET) { const claim = await this.bridgeL1FeeJuice(account); const paymentMethod = new FeeJuicePaymentMethodWithClaim(account, claim); - const { estimatedGas } = await minimalInteraction.simulate({ - from: account, - fee: { estimateGas: true, paymentMethod }, - }); - const gasSettings = GasSettings.from({ - ...estimatedGas!, + // Leave gas limits unset so the wallet derives them from its own simulation and clamps to the + // network's per-tx admission limits. + const gasSettings = { maxFeesPerGas, maxPriorityFeesPerGas: GasFees.empty(), - }); + }; await this.withNoMinTxsPerBlock(async () => { const { txHash } = await minimalInteraction.send({ diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index 0bf8ad99a8d5..8250f0388444 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -89,10 +89,10 @@ export async function createAccount( const deployMethod = await account.getDeployMethod(); const sim = await deployMethod.simulate({ ...deployAccountOpts, - fee: { ...deployAccountOpts.fee, estimateGas: true }, + includeMetadata: true, }); - // estimateGas: true guarantees these fields are present - const estimatedGas = sim.estimatedGas!; + // includeMetadata: true guarantees these fields are present + const estimatedGas = await wallet.estimateGasLimits(sim.gasUsed!); const stats = sim.stats!; if (feeOpts.estimateOnly) { diff --git a/yarn-project/cli-wallet/src/cmds/deploy.ts b/yarn-project/cli-wallet/src/cmds/deploy.ts index d3da0565d9f7..4d6f559a01c3 100644 --- a/yarn-project/cli-wallet/src/cmds/deploy.ts +++ b/yarn-project/cli-wallet/src/cmds/deploy.ts @@ -75,10 +75,10 @@ export async function deploy( const localStart = performance.now(); const sim = await deployInteraction.simulate({ ...deployOpts, - fee: { ...deployOpts.fee, estimateGas: true }, + includeMetadata: true, }); - // estimateGas: true guarantees these fields are present - const estimatedGas = sim.estimatedGas!; + // includeMetadata: true guarantees these fields are present + const estimatedGas = await wallet.estimateGasLimits(sim.gasUsed!); const stats = sim.stats!; if (feeOpts.estimateOnly) { diff --git a/yarn-project/cli-wallet/src/cmds/deploy_account.ts b/yarn-project/cli-wallet/src/cmds/deploy_account.ts index ca5b5ee10304..5177e426fd3e 100644 --- a/yarn-project/cli-wallet/src/cmds/deploy_account.ts +++ b/yarn-project/cli-wallet/src/cmds/deploy_account.ts @@ -68,10 +68,10 @@ export async function deployAccount( const deployMethod = await account.getDeployMethod(); const sim = await deployMethod.simulate({ ...deployAccountOpts, - fee: { ...deployAccountOpts.fee, estimateGas: true }, + includeMetadata: true, }); - // estimateGas: true guarantees these fields are present - const estimatedGas = sim.estimatedGas!; + // includeMetadata: true guarantees these fields are present + const estimatedGas = await wallet.estimateGasLimits(sim.gasUsed!); const stats = sim.stats!; if (feeOpts.estimateOnly) { diff --git a/yarn-project/cli-wallet/src/cmds/send.ts b/yarn-project/cli-wallet/src/cmds/send.ts index 5d48d96aafba..195f9de1aa1a 100644 --- a/yarn-project/cli-wallet/src/cmds/send.ts +++ b/yarn-project/cli-wallet/src/cmds/send.ts @@ -42,10 +42,10 @@ export async function send( const localStart = performance.now(); const sim = await call.simulate({ ...sendOptions, - fee: { ...sendOptions.fee, estimateGas: true }, + includeMetadata: true, }); - // estimateGas: true guarantees these fields are present - const estimatedGas = sim.estimatedGas!; + // includeMetadata: true guarantees these fields are present + const estimatedGas = await wallet.estimateGasLimits(sim.gasUsed!); const stats = sim.stats!; if (feeOpts.estimateOnly) { diff --git a/yarn-project/cli-wallet/src/utils/options/fees.ts b/yarn-project/cli-wallet/src/utils/options/fees.ts index 82cea4f5aca4..5628955fc5a2 100644 --- a/yarn-project/cli-wallet/src/utils/options/fees.ts +++ b/yarn-project/cli-wallet/src/utils/options/fees.ts @@ -259,7 +259,9 @@ export class CLIFeeArgs { async toUserFeeOptions(node: AztecNode, wallet: Wallet, from: AztecAddress): Promise { const minFees = await this.getMinFees(node); const maxFeesPerGas = minFees.mul(1 + MIN_FEE_PADDING); - const gasSettings = GasSettings.fallback({ ...this.gasSettings, maxFeesPerGas }); + const { txsLimits } = await node.getNodeInfo(); + const gasLimits = this.gasSettings.gasLimits ?? Gas.from(txsLimits.gas); + const gasSettings = GasSettings.fallback({ ...this.gasSettings, gasLimits, maxFeesPerGas }); const paymentMethod = await this.paymentMethod(wallet, from, gasSettings); return { paymentMethod, diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index e4ff836941de..212e42c3f6d4 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -4,9 +4,9 @@ import { SchnorrAccountContract } from '@aztec/accounts/schnorr'; import { StubSchnorrAccountContractArtifact, createStubSchnorrAccount } from '@aztec/accounts/schnorr/stub'; import { getIdentities } from '@aztec/accounts/utils'; import { type Account, type AccountContract, NO_FROM } from '@aztec/aztec.js/account'; -import { type InteractionFeeOptions, getContractClassFromArtifact, getGasLimits } from '@aztec/aztec.js/contracts'; +import { type InteractionFeeOptions, getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; import type { AztecNode } from '@aztec/aztec.js/node'; -import { AccountManager, type Aliased, type SimulateOptions } from '@aztec/aztec.js/wallet'; +import { AccountManager, type Aliased } from '@aztec/aztec.js/wallet'; import { TxSimulationResultWithAppOffset } from '@aztec/aztec.js/wallet'; import type { DefaultAccountEntrypointOptions } from '@aztec/entrypoints/account'; import { DefaultEntrypoint } from '@aztec/entrypoints/default'; @@ -18,16 +18,19 @@ import type { PXE } from '@aztec/pxe/server'; import { createPXE, getPXEConfig } from '@aztec/pxe/server'; import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { Gas, GasUsed } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { NoteDao } from '@aztec/stdlib/note'; import type { SimulationOverrides, TxExecutionRequest, TxProvingResult } from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; -import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; +import { BaseWallet, type SimulateViaEntrypointOptions, getGasLimits } from '@aztec/wallet-sdk/base-wallet'; import type { WalletDB } from '../storage/wallet_db.js'; import type { AccountType } from './constants.js'; import { extractECDSAPublicKeyFromBase64String } from './ecdsa.js'; -import { printGasEstimates } from './options/fees.js'; + +/** Padding the CLI wallet applies to simulated gas usage when deriving declared gas limits. */ +const DEFAULT_ESTIMATED_GAS_PADDING = 0.1; export class CLIWallet extends BaseWallet { private accountCache = new Map(); @@ -92,6 +95,16 @@ export class CLIWallet extends BaseWallet { ); } + /** + * Derives suggested total and teardown gas limits from simulated gas usage, padded and clamped to the + * network's per-tx admission limits. + * @param gasUsed - The gas consumed during simulation (from a `simulate({ includeMetadata: true })` result). + */ + async estimateGasLimits(gasUsed: GasUsed): Promise<{ gasLimits: Gas; teardownGasLimits: Gas }> { + const maxTxGasLimits = await this.getMaxTxGasLimits(); + return getGasLimits(gasUsed, maxTxGasLimits, DEFAULT_ESTIMATED_GAS_PADDING); + } + private async createCancellationTxExecutionRequest( from: AztecAddress, txNonce: Fr, @@ -240,24 +253,6 @@ export class CLIWallet extends BaseWallet { return { account: stubAccount, instance }; } - override async simulateTx( - executionPayload: ExecutionPayload, - opts: SimulateOptions, - ): Promise { - const simulationResults = await super.simulateTx(executionPayload, opts); - - if (opts.fee?.estimateGas) { - const feeOptions = await this.completeFeeOptions({ - from: opts.from, - feePayer: executionPayload.feePayer, - gasSettings: opts.fee?.gasSettings, - }); - const limits = getGasLimits(simulationResults, opts.fee?.estimatedGasPadding); - printGasEstimates(feeOptions, limits, this.userLog); - } - return simulationResults; - } - /** * Uses a stub account for kernelless simulation, bypassing real account authorization. * Uses DefaultEntrypoint directly for NO_FROM transactions. diff --git a/yarn-project/cli-wallet/test/flows/shared/setup.sh b/yarn-project/cli-wallet/test/flows/shared/setup.sh index 87864ec20965..28a9b48efd51 100644 --- a/yarn-project/cli-wallet/test/flows/shared/setup.sh +++ b/yarn-project/cli-wallet/test/flows/shared/setup.sh @@ -18,20 +18,7 @@ export PXE_PROVER="none" function aztec-wallet { echo_header aztec-wallet "$@" - # These flows run serially against the single-block local-network sandbox, where a proposed block can - # be orphaned and pruned before its checkpoint is published, dropping a tx we already moved on from - # and breaking a later step that depends on it (e.g. the fee-juice claim consuming a bridged L1->L2 - # message). Wait for the tx-producing commands to be checkpointed so each is durably included before - # the next is sent. Scoped to these tests only; the cli-wallet default stays 'proposed'. - local wait_for_checkpointed=() - case "$1" in - send | deploy | deploy-account | create-account) - if [[ "$*" != *"--no-wait"* && "$*" != *"--wait-for-status"* ]]; then - wait_for_checkpointed=(--wait-for-status checkpointed) - fi - ;; - esac - $command "$@" ${wait_for_checkpointed[@]+"${wait_for_checkpointed[@]}"} + $command "$@" } function assert_eq { diff --git a/yarn-project/constants/src/constants.ts b/yarn-project/constants/src/constants.ts index 6f76506fcccc..e07b5e1303af 100644 --- a/yarn-project/constants/src/constants.ts +++ b/yarn-project/constants/src/constants.ts @@ -8,6 +8,7 @@ import { GENESIS_BLOCK_HEADER_HASH as GENESIS_BLOCK_HEADER_HASH_BIGINT, INITIAL_CHECKPOINT_NUMBER as INITIAL_CHECKPOINT_NUM_RAW, INITIAL_L2_BLOCK_NUM as INITIAL_L2_BLOCK_NUM_RAW, + MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT as MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT_RAW, MAX_TX_BLOB_DATA_SIZE_IN_FIELDS, } from './constants.gen.js'; @@ -36,3 +37,15 @@ export const INITIAL_CHECKPOINT_NUMBER: CheckpointNumber = CheckpointNumber(INIT /** The block header hash for the genesis block 0. */ // eslint-disable-next-line import-x/export export const GENESIS_BLOCK_HEADER_HASH = new Fr(GENESIS_BLOCK_HEADER_HASH_BIGINT); + +/** + * The RAW DA capacity of a checkpoint's blobs: `BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB * DA_GAS_PER_FIELD`. + * This value is NOT attainable by transactions. The blob encoding reserves overhead fields that no tx pays + * DA gas for: a checkpoint-end marker field plus per-block block-end fields (7 for the first block in a + * checkpoint, 6 for each subsequent block — see `@aztec/blob-lib` encoding). The true tx-usable DA budget + * is lower and block-count-dependent; consumers budgeting tx data should subtract the encoding overhead (as + * `CheckpointBuilder` and the network tx gas limits in `@aztec/stdlib/gas` do) rather than use this value + * directly. + */ +// eslint-disable-next-line import-x/export +export const MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT_RAW; diff --git a/yarn-project/end-to-end/bootstrap.sh b/yarn-project/end-to-end/bootstrap.sh index 18fd910e9372..3c9806c4985c 100755 --- a/yarn-project/end-to-end/bootstrap.sh +++ b/yarn-project/end-to-end/bootstrap.sh @@ -96,7 +96,13 @@ function test_cmds { ) for test in "${tests[@]}"; do # We must set ONLY_TERM_PARENT=1 to allow the script to fully control cleanup process. - echo "$hash:ONLY_TERM_PARENT=1:TIMEOUT=30m $run_test_script ha $test" + if [[ "$test" == *.parallel.test.ts ]]; then + while IFS= read -r test_name; do + echo "$hash:ONLY_TERM_PARENT=1:TIMEOUT=30m $run_test_script ha $test \"$test_name\"" + done < <(extract_test_names "$test") + else + echo "$hash:ONLY_TERM_PARENT=1:TIMEOUT=30m $run_test_script ha $test" + fi done #echo "$hash:ONLY_TERM_PARENT=1 $run_test_script simple src/e2e_multi_validator/e2e_multi_validator_node.test.ts" diff --git a/yarn-project/end-to-end/scripts/ha/docker-compose.yml b/yarn-project/end-to-end/scripts/ha/docker-compose.yml index eb8ecad5d320..cb700f840159 100644 --- a/yarn-project/end-to-end/scripts/ha/docker-compose.yml +++ b/yarn-project/end-to-end/scripts/ha/docker-compose.yml @@ -29,12 +29,6 @@ services: volumes: - web3signer_keys:/keys - anvil: - image: aztecprotocol/build:3.0 - cpus: 1 - mem_limit: 2G - entrypoint: 'anvil --silent -p 8545 --host 0.0.0.0 --chain-id 31337' - end-to-end: image: aztecprotocol/build:3.0 cpus: 4 @@ -51,7 +45,8 @@ services: environment: JEST_CACHE_DIR: /tmp-jest LOG_LEVEL: ${LOG_LEVEL:-verbose} - ETHEREUM_HOSTS: http://anvil:8545 + TEST: ${TEST:-./src/composed/ha/e2e_ha_full.parallel.test.ts} + TEST_NAME: ${TEST_NAME:-} L1_CHAIN_ID: 31337 DATABASE_URL: postgresql://aztec:aztec@postgres:5432/aztec_ha_test WEB3_SIGNER_URL: http://web3signer:9000 @@ -70,10 +65,6 @@ services: while ! nc -z web3signer 9000; do sleep 1; done; echo "Web3Signer is ready" - # Wait for anvil to be ready - while ! nc -z anvil 8545; do sleep 1; done; - echo "Anvil is ready" - # Run database migrations echo "Running database migrations..." cd /root/aztec-packages/yarn-project/aztec @@ -84,7 +75,7 @@ services: cd /root/aztec-packages/yarn-project/end-to-end # Run the test - setsid ./scripts/test_simple.sh ${TEST:-./src/composed/ha/e2e_ha_sequencer.test.ts} & + setsid ./scripts/test_simple.sh "$${TEST}" "$${TEST_NAME}" & pid=$$! pgid=$$(($$(ps -o pgid= -p $$pid))) trap "kill -SIGTERM -$$pgid" SIGTERM @@ -96,8 +87,6 @@ services: condition: service_healthy web3signer: condition: service_started - anvil: - condition: service_started volumes: postgres_data: diff --git a/yarn-project/end-to-end/scripts/run_test.sh b/yarn-project/end-to-end/scripts/run_test.sh index 7f5ca7b8219a..c5a06472e5e5 100755 --- a/yarn-project/end-to-end/scripts/run_test.sh +++ b/yarn-project/end-to-end/scripts/run_test.sh @@ -25,7 +25,10 @@ case "$type" in TEST=$test exec run_compose_test $test end-to-end $PWD/web3signer ;; "ha") - # Remove volumes on cleanup for HA tests to ensure clean database state on retries - TEST=$test REMOVE_COMPOSE_VOLUMES=1 exec run_compose_test $test end-to-end $PWD/ha + # Remove volumes on cleanup for HA tests to ensure clean database state on retries. + # NAME_POSTFIX namespaces the compose project per test so parallel per-test jobs don't collide. + # Compose project names must be lowercase alphanumerics, hyphens, and underscores. + postfix=$(echo "$test_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g') + TEST=$test TEST_NAME=$test_name NAME_POSTFIX=${postfix:+_$postfix} REMOVE_COMPOSE_VOLUMES=1 exec run_compose_test $test end-to-end $PWD/ha ;; esac 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 e07f11eed7f9..c0d4d0ccfbf2 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 @@ -5,6 +5,11 @@ import { Metrics } from '@aztec/telemetry-client'; import type { EndToEndContext } from '../fixtures/utils.js'; import { benchmarkSetup, sendTxs, waitTxs } from './utils.js'; +const AZTEC_SLOT_DURATION_SECONDS = 600; +const ETHEREUM_SLOT_DURATION_SECONDS = 12; +const BLOCK_DURATION_MS = 200_000; +const L1_TX_TIMEOUT_MS = 30 * 60 * 1000; + describe('benchmarks/build_block', () => { let context: EndToEndContext; let contract: BenchmarkingContract; @@ -13,7 +18,20 @@ describe('benchmarks/build_block', () => { beforeEach(async () => { ({ context, contract, sequencer } = await benchmarkSetup({ maxTxsPerBlock: 1024, - enforceTimeTable: false, // Let the sequencer take as much time as it needs + // The timetable is now always enforced, so give the single bench block enough headroom that + // it never hits a sub-slot build deadline (we want to measure pure build time, not a + // deadline-truncated block). With aztecSlotDuration=600s and ethereumSlotDuration=12s there is + // no sub-8s normalization, so init=1s, assemble=1s, P=2s. The model requires + // timeAvailableForBlocks = S - init - (assemble + 2P + D) >= D + // => 600 - 1 - (1 + 4 + 200) = 394 >= 200, giving maxBlocksPerSlot = floor(394/200) = 1. + // The first (and only) sub-slot's build deadline is init + D = 201s into the slot, far more + // than 32 txs need. + aztecSlotDuration: AZTEC_SLOT_DURATION_SECONDS, + ethereumSlotDuration: ETHEREUM_SLOT_DURATION_SECONDS, + blockDurationMs: BLOCK_DURATION_MS, + enableDelayer: false, + txTimeoutMs: L1_TX_TIMEOUT_MS, + txCancellationFinalTimeoutMs: L1_TX_TIMEOUT_MS, metrics: [ Metrics.SEQUENCER_BLOCK_BUILD_DURATION, { diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index 74ba3a4439d7..5bcd3495fb0f 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -25,7 +25,7 @@ import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { type PXEConfig, getPXEConfig } from '@aztec/pxe/server'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasSettings } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { AUTOMINE_E2E_OPTS, MNEMONIC, getPaddedMaxFeesPerGas } from '../../fixtures/fixtures.js'; @@ -389,7 +389,8 @@ export class ClientFlowsBenchmark { // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay const maxFeesPerGas = await getPaddedMaxFeesPerGas(this.aztecNode); - const gasSettings = GasSettings.fallback({ maxFeesPerGas }); + const gasLimits = Gas.from((await this.aztecNode.getNodeInfo()).txsLimits.gas); + const gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); return new PrivateFeePaymentMethod(this.bananaFPC.address, sender, wallet, gasSettings); } diff --git a/yarn-project/end-to-end/src/bench/utils.ts b/yarn-project/end-to-end/src/bench/utils.ts index 78834612d929..9920c4ae5e7a 100644 --- a/yarn-project/end-to-end/src/bench/utils.ts +++ b/yarn-project/end-to-end/src/bench/utils.ts @@ -1,9 +1,14 @@ +import type { InitialAccountData } from '@aztec/accounts/testing'; import type { AztecNodeService } from '@aztec/aztec-node'; +import { getAccountContractAddress } from '@aztec/aztec.js/account'; import { AztecAddress } from '@aztec/aztec.js/addresses'; import { BatchCall, NO_WAIT, type WaitOpts } from '@aztec/aztec.js/contracts'; +import { Fr } from '@aztec/aztec.js/fields'; import { waitForTx } from '@aztec/aztec.js/node'; +import { SlotNumber } from '@aztec/foundation/branded-types'; import { mean, stdDev, times } from '@aztec/foundation/collection'; import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; +import { type Sequencer, type SequencerEvents, SequencerState } from '@aztec/sequencer-client'; import type { TxHash } from '@aztec/stdlib/tx'; import type { MetricDefinition } from '@aztec/telemetry-client'; import type { BenchmarkDataPoint, BenchmarkMetricsType, BenchmarkTelemetryClient } from '@aztec/telemetry-client/bench'; @@ -12,10 +17,19 @@ import { mkdirSync, writeFileSync } from 'fs'; import path from 'path'; import { PIPELINING_SETUP_OPTS } from '../fixtures/fixtures.js'; +import { + SCHNORR_HARDCODED_PRIVATE_KEY, + SchnorrHardcodedKeyAccountContract, +} from '../fixtures/schnorr_hardcoded_account_contract.js'; import { type EndToEndContext, type SetupOptions, setup } from '../fixtures/utils.js'; +const MAX_BENCH_WAIT_BLOCKS = 6; +const BENCH_FINAL_WAIT_TIMEOUT_SECONDS = 10; +const SEQUENCER_IDLE_TIMEOUT_MS = 60_000; +const DEFAULT_L1_PUBLISHING_TIME_SECONDS = 12; + /** - * Setup for benchmarks. Initializes a remote node with a single account and deploys a benchmark contract. + * Setup for benchmarks. Initializes a remote node with a funded hardcoded account and deploys a benchmark contract. */ export async function benchmarkSetup( opts: Partial & { @@ -24,11 +38,28 @@ export async function benchmarkSetup( benchOutput?: string; }, ) { - const context = await setup(1, { ...PIPELINING_SETUP_OPTS, ...opts, telemetryConfig: { benchmark: true } }); - const defaultAccountAddress = context.accounts[0]; - const { contract } = await BenchmarkingContract.deploy(context.wallet).send({ from: defaultAccountAddress }); + const benchmarkAccountData = await getBenchmarkAccountData(); + const context = await setup(0, { + ...PIPELINING_SETUP_OPTS, + ...opts, + initialFundedAccounts: [benchmarkAccountData], + skipAccountDeployment: true, + telemetryConfig: { benchmark: true }, + }); + const accountManager = await context.wallet.createAccount({ + secret: benchmarkAccountData.secret, + salt: benchmarkAccountData.salt, + contract: new SchnorrHardcodedKeyAccountContract(), + }); + const defaultAccountAddress = accountManager.address; + context.accounts = [defaultAccountAddress]; + const sequencer = getSequencerClient(context); + await waitForSequencerIdle(sequencer.getSequencer()); + const deployment = BenchmarkingContract.deploy(context.wallet); + const { txHash } = await deployment.send({ from: defaultAccountAddress, wait: NO_WAIT }); + await waitTxs([txHash], context); + const contract = await deployment.register(); context.logger.info(`Deployed benchmarking contract at ${contract.address}`); - const sequencer = (context.aztecNode as AztecNodeService).getSequencer()!; const telemetry = context.telemetryClient as BenchmarkTelemetryClient; context.logger.warn(`Cleared benchmark data points from setup`); telemetry.clear(); @@ -49,6 +80,22 @@ export async function benchmarkSetup( return { telemetry, context, contract, sequencer }; } +async function getBenchmarkAccountData(): Promise { + const contract = new SchnorrHardcodedKeyAccountContract(); + const secret = Fr.random(); + const salt = Fr.random(); + const address = await getAccountContractAddress(contract, secret, salt); + return { secret, salt, signingKey: SCHNORR_HARDCODED_PRIVATE_KEY, address }; +} + +function getSequencerClient(context: EndToEndContext) { + const sequencer = (context.aztecNode as AztecNodeService).getSequencer(); + if (!sequencer) { + throw new Error('Benchmark setup requires a local sequencer'); + } + return sequencer; +} + type MetricFilter = { source: MetricDefinition; transform: (value: number) => number; @@ -160,10 +207,90 @@ export async function sendTxs( export async function waitTxs(txs: TxHash[], context: EndToEndContext, txWaitOpts?: WaitOpts) { context.logger.info(`Awaiting ${txs.length} txs to be mined`); - await Promise.all(txs.map(txHash => waitForTx(context.aztecNode, txHash, txWaitOpts))); + await waitTxsWithBlockMining(txs, context, txWaitOpts); context.logger.info(`${txs.length} txs have been mined`); } +async function waitTxsWithBlockMining(txs: TxHash[], context: EndToEndContext, txWaitOpts?: WaitOpts) { + const sequencer = getSequencerClient(context).getSequencer(); + for (let attempt = 0; attempt <= MAX_BENCH_WAIT_BLOCKS; attempt++) { + if (await haveAllTxsMined(txs, context, txWaitOpts)) { + await waitForSequencerIdle(sequencer); + return; + } + await waitForSequencerIdle(sequencer); + context.logger.info('Mining next benchmark block while waiting for txs', { + attempt, + txs: txs.length, + }); + await mineNextBenchmarkBlock(context, sequencer); + } + await Promise.all( + txs.map(txHash => + waitForTx(context.aztecNode, txHash, { + ...txWaitOpts, + timeout: txWaitOpts?.timeout ?? BENCH_FINAL_WAIT_TIMEOUT_SECONDS, + }), + ), + ); +} + +async function haveAllTxsMined(txs: TxHash[], context: EndToEndContext, txWaitOpts?: WaitOpts): Promise { + const receipts = await Promise.all(txs.map(txHash => context.aztecNode.getTxReceipt(txHash))); + for (const receipt of receipts) { + if (receipt.isDropped()) { + throw new Error(`Transaction ${receipt.txHash.toString()} was dropped. Reason: ${receipt.error ?? 'unknown'}`); + } + if (!receipt.isMined()) { + return false; + } + if (!receipt.hasExecutionSucceeded() && !txWaitOpts?.dontThrowOnRevert) { + throw new Error( + `Transaction ${receipt.txHash.toString()} reverted: ${receipt.executionResult}. Reason: ${ + receipt.error ?? 'unknown' + }`, + ); + } + } + return true; +} + +async function mineNextBenchmarkBlock(context: EndToEndContext, sequencer: Sequencer) { + const l1PublishingTime = BigInt(context.config.l1PublishingTime ?? DEFAULT_L1_PUBLISHING_TIME_SECONDS); + const currentTimestamp = BigInt(await context.cheatCodes.eth.lastBlockTimestamp()); + let targetSlot = SlotNumber((await context.cheatCodes.rollup.getSlot()) + 1); + let targetTimestamp = await context.cheatCodes.rollup.getTimestampForSlot(targetSlot); + while (targetTimestamp - l1PublishingTime - 1n <= currentTimestamp) { + targetSlot = SlotNumber(targetSlot + 1); + targetTimestamp = await context.cheatCodes.rollup.getTimestampForSlot(targetSlot); + } + + const triggerTimestamp = targetTimestamp - l1PublishingTime - 1n; + await context.cheatCodes.eth.warp(triggerTimestamp, { resetBlockInterval: true }); + await context.aztecNode.mineBlock(); + await waitForSequencerIdle(sequencer); +} + +function waitForSequencerIdle(sequencer: Sequencer, timeout = SEQUENCER_IDLE_TIMEOUT_MS): Promise { + if (sequencer.status().state === SequencerState.IDLE) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + sequencer.off('state-changed', handler); + reject(new Error('Timeout waiting for sequencer IDLE state')); + }, timeout); + const handler = (args: Parameters[0]) => { + if (args.newState === SequencerState.IDLE) { + clearTimeout(timer); + sequencer.off('state-changed', handler); + resolve(); + } + }; + sequencer.on('state-changed', handler); + }); +} + function randomBytesAsBigInts(length: number): bigint[] { return [...Array(length)].map(_ => BigInt(Math.floor(Math.random() * 255))); } 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 be5c43d80cb4..e02fe9dcf496 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 @@ -11,7 +11,7 @@ import { createAztecNodeClient, waitForNode } from '@aztec/aztec.js/node'; import { getFeeJuiceBalance } from '@aztec/aztec.js/utils'; import { timesParallel } from '@aztec/foundation/collection'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import { GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasSettings } from '@aztec/stdlib/gas'; import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing'; import { format } from 'util'; @@ -183,7 +183,8 @@ describe('e2e_local_network_example', () => { // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay const maxFeesPerGas = await getPaddedMaxFeesPerGas(node); - const gasSettings = GasSettings.fallback({ maxFeesPerGas }); + const gasLimits = Gas.from((await node.getNodeInfo()).txsLimits.gas); + const gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPCAddress, alice, wallet, gasSettings); const { receipt: receiptForAlice } = await bananaCoin.methods .transfer(bob, amountTransferToBob) 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 a5cae6862da4..e5947a5dc11c 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 @@ -6,9 +6,11 @@ import { L1TokenManager, L1TokenPortalManager } from '@aztec/aztec.js/ethereum'; import { Fr } from '@aztec/aztec.js/fields'; import { createLogger } from '@aztec/aztec.js/log'; import { createAztecNodeClient, waitForNode } from '@aztec/aztec.js/node'; +import { CheatCodes } from '@aztec/aztec/testing'; import { createExtendedL1Client } from '@aztec/ethereum/client'; import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract'; import { retryUntil } from '@aztec/foundation/retry'; +import { DateProvider } from '@aztec/foundation/timer'; import { FeeAssetHandlerAbi, FeeAssetHandlerBytecode, @@ -40,7 +42,8 @@ const setupLocalNetwork = async () => { const node = createAztecNodeClient(AZTEC_NODE_URL); await waitForNode(node); const wallet = await TestWallet.create(node); - return { node, wallet }; + const cheatCodes = await CheatCodes.create(ETHEREUM_HOSTS.split(','), node, new DateProvider()); + return { cheatCodes, node, wallet }; }; async function deployTestERC20(): Promise { @@ -84,7 +87,7 @@ async function addMinter(l1TokenContract: EthAddress, l1TokenHandler: EthAddress 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'); - const { wallet, node } = await setupLocalNetwork(); + const { cheatCodes, wallet, node } = await setupLocalNetwork(); const [ownerAztecAddress] = await registerInitialLocalNetworkAccountsInWallet(wallet); const l1ContractAddresses = (await node.getNodeInfo()).l1ContractAddresses; logger.info('L1 Contract Addresses:'); @@ -203,6 +206,21 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { const { receipt: l2TxReceipt } = await l2BridgeContract.methods .exit_to_l1_public(EthAddress.fromString(ownerEthAddress), withdrawAmount, EthAddress.ZERO, authwitNonce) .send({ from: ownerAztecAddress }); + const l2ExitBlock = await retryUntil(() => node.getBlock(l2TxReceipt.blockNumber!), 'L2 exit block', 120, 1); + const result = await retryUntil( + () => node.getL2ToL1MembershipWitness(l2TxReceipt.txHash, l2ToL1Message), + 'l2 to l1 membership witness', + 120, + 1, + ); + await cheatCodes.rollup.markAsProven(l2ExitBlock.checkpointNumber); + await cheatCodes.eth.mine(); + await retryUntil( + async () => (await node.getBlockNumber('proven')) >= l2TxReceipt.blockNumber!, + 'mark L2 exit checkpoint proven', + 120, + 1, + ); await waitForProven(node, l2TxReceipt, { provenTimeout: 500 }); const { result: newL2Balance } = await l2TokenContract.methods @@ -210,13 +228,6 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { .simulate({ from: ownerAztecAddress }); logger.info(`New L2 balance of ${ownerAztecAddress} is ${newL2Balance}`); - const result = await retryUntil( - () => node.getL2ToL1MembershipWitness(l2TxReceipt.txHash, l2ToL1Message), - 'l2 to l1 membership witness', - 60, - 1, - ); - await l1PortalManager.withdrawFunds( withdrawAmount, EthAddress.fromString(ownerEthAddress), diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts similarity index 84% rename from yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts rename to yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts index d59e3cb189c9..87c7617c5844 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.parallel.test.ts @@ -7,13 +7,12 @@ */ import type { InitialAccountData } from '@aztec/accounts/testing'; import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; -import { NO_FROM } from '@aztec/aztec.js/account'; +import { getAccountContractAddress } from '@aztec/aztec.js/account'; import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; -import { waitForProven } from '@aztec/aztec.js/contracts'; -import { ContractDeployer } from '@aztec/aztec.js/deployment'; +import { NO_WAIT, getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; import { Fr } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; -import type { AztecNode } from '@aztec/aztec.js/node'; +import { type AztecNode, waitForTx } from '@aztec/aztec.js/node'; import { GovernanceProposerContract } from '@aztec/ethereum/contracts'; import type { DeployAztecL1ContractsReturnType } from '@aztec/ethereum/deploy-aztec-l1-contracts'; import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; @@ -24,12 +23,12 @@ import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { TestDateProvider } from '@aztec/foundation/timer'; import { GovernanceProposerAbi } from '@aztec/l1-artifacts/GovernanceProposerAbi'; -import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest'; +import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { type AttestationInfo, getAttestationInfoFromPublishedCheckpoint } from '@aztec/stdlib/block'; import { Checkpoint } from '@aztec/stdlib/checkpoint'; import { TopicType } from '@aztec/stdlib/p2p'; import { OffenseType } from '@aztec/stdlib/slashing'; -import { TxStatus } from '@aztec/stdlib/tx'; +import { TxHash, type TxReceipt, TxStatus } from '@aztec/stdlib/tx'; import type { GenesisData } from '@aztec/stdlib/world-state'; import type { ValidatorClient } from '@aztec/validator-client'; import { PostgresSlashingProtectionDatabase } from '@aztec/validator-ha-signer/db'; @@ -52,6 +51,10 @@ import { setupHADatabase, verifyNoDuplicateAttestations, } from '../../fixtures/ha_setup.js'; +import { + SCHNORR_HARDCODED_PRIVATE_KEY, + SchnorrHardcodedKeyAccountContract, +} from '../../fixtures/schnorr_hardcoded_account_contract.js'; import { getPrivateKeyFromIndex, setup } from '../../fixtures/utils.js'; import { createWeb3SignerKeystore, @@ -60,27 +63,70 @@ import { refreshWeb3Signer, } from '../../fixtures/web3signer.js'; import type { TestWallet } from '../../test-wallet/test_wallet.js'; +import { proveInteraction } from '../../test-wallet/utils.js'; const NODE_COUNT = 5; const VALIDATOR_COUNT = 4; const COMMITTEE_SIZE = 4; +async function getHardcodedAccountData(secret: Fr, salt: Fr): Promise { + const contract = new SchnorrHardcodedKeyAccountContract(); + const address = await getAccountContractAddress(contract, secret, salt); + return { secret, salt, signingKey: SCHNORR_HARDCODED_PRIVATE_KEY, address }; +} + +async function registerHardcodedAccount(wallet: TestWallet, accountData: InitialAccountData): Promise { + const accountManager = await wallet.createAccount({ + secret: accountData.secret, + salt: accountData.salt, + contract: new SchnorrHardcodedKeyAccountContract(), + }); + return accountManager.address; +} + +async function registerTestContract(wallet: TestWallet): Promise { + const instance = await getContractInstanceFromInstantiationParams(TestContract.artifact, { + constructorArgs: [], + constructorArtifact: undefined, + salt: Fr.ZERO, + publicKeys: undefined, + deployer: undefined, + }); + await wallet.registerContract(instance, TestContract.artifact); + return TestContract.at(instance.address, wallet); +} + +async function submitTriggerTx(wallet: TestWallet, testContract: TestContract, from: AztecAddress): Promise { + const tx = await proveInteraction(wallet, testContract.methods.emit_nullifier(Fr.random()), { from }); + return await tx.send({ wait: NO_WAIT }); +} + +async function waitForTriggerTx(node: AztecNode, txHash: TxHash): Promise { + const receipt = await waitForTx(node, txHash, { waitForStatus: TxStatus.CHECKPOINTED }); + if (!receipt.blockNumber) { + throw new Error('Trigger tx was checkpointed without a block number'); + } + return receipt; +} + describe('HA Full Setup', () => { jest.setTimeout(20 * 60 * 1000); // 20 minutes let logger: Logger; let wallet: TestWallet; let ownerAddress: AztecAddress; + let testContract: TestContract; let aztecNode: AztecNode; let config: AztecNodeConfig; - let teardown: () => Promise; - let initialFundedAccounts: InitialAccountData[]; + let teardown: () => Promise = async () => {}; let dateProvider: TestDateProvider; let genesis: GenesisData | undefined; // HA specific resources let haNodePools: Pool[]; // Database pools for HA nodes (for cleanup) let haNodeServices: AztecNodeService[]; // All N HA peer nodes + let haSequencersStarted = false; + const stoppedHANodeIndexes = new Set(); let haKeystoreDirs: string[]; let mainPool: Pool; let databaseConfig: HADatabaseConfig; @@ -98,6 +144,37 @@ describe('HA Full Setup', () => { rollupAddress: deployL1ContractsValues.l1ContractAddresses.rollupAddress, }); + const startHASequencers = async () => { + if (haSequencersStarted) { + return; + } + + await Promise.all( + haNodeServices.map(async (service, i) => { + logger.info(`Starting HA peer node ${i} sequencer`); + await service.getSequencer()?.start(); + }), + ); + haSequencersStarted = true; + logger.info('All HA peer sequencers started'); + }; + + const sendTriggerTx = async (): Promise => { + await startHASequencers(); + const txHash = await submitTriggerTx(wallet, testContract, ownerAddress); + return await waitForTriggerTx(aztecNode, txHash); + }; + + const stopHANode = async (nodeIndex: number) => { + if (stoppedHANodeIndexes.has(nodeIndex)) { + return; + } + + logger.info(`Stopping HA peer node ${nodeIndex}`); + await haNodeServices[nodeIndex].stop(); + stoppedHANodeIndexes.add(nodeIndex); + }; + beforeAll(async () => { // Check required environment variables if (!process.env.DATABASE_URL) { @@ -145,30 +222,30 @@ describe('HA Full Setup', () => { ); const initialValidators = createInitialValidatorsFromPrivateKeys(attesterPrivateKeys); + const hardcodedAccountData = await getHardcodedAccountData(Fr.random(), Fr.random()); - ({ - teardown, - logger, - wallet, - aztecNode, - config, - initialFundedAccounts, - dateProvider, - deployL1ContractsValues, - genesis, - } = await setup( - 1, + ({ teardown, logger, wallet, aztecNode, config, dateProvider, deployL1ContractsValues, genesis } = await setup( + 0, { ...PIPELINING_SETUP_OPTS, + automineL1Setup: true, + initialFundedAccounts: [hardcodedAccountData], initialValidators, sequencerPublisherPrivateKeys: [new SecretValue(publisherPrivateKeys[0])], aztecTargetCommitteeSize: COMMITTEE_SIZE, + // The full HA docker/Web3Signer stack can still be joining and syncing after the shared + // 12s pipelining preset's 2.5s start window has closed. Keep real sequencing, but give + // HA validators enough time to pass the enforced build-start gate in CI. + aztecSlotDuration: 16, + // This suite validates HA coordination on tx-bearing checkpoints. Requiring one tx avoids a startup empty + // checkpoint from occupying the shared HA publisher while the trigger tx is still being prepared. + minTxsPerBlock: 1, archiverPollingIntervalMS: 200, sequencerPollingIntervalMS: 200, worldStateBlockCheckIntervalMS: 200, blockCheckIntervalMS: 200, startProverNode: true, - // Disable validation on this node + // The bootstrap node is only an RPC/P2P anchor. HA validators are the first block producers in this suite. disableValidator: true, skipAccountDeployment: true, // Enable P2P for transaction gossip @@ -181,11 +258,14 @@ describe('HA Full Setup', () => { { syncChainTip: 'proven' }, )); + ownerAddress = await registerHardcodedAccount(wallet, hardcodedAccountData); + testContract = await registerTestContract(wallet); + if (!dateProvider) { throw new Error('dateProvider must be provided by setup for HA tests'); } - logger.info(`Bootstrap node setup complete (validation disabled)`); + logger.info('Bootstrap node setup complete; registered funded hardcoded account and test contract locally'); // Get bootstrap node's P2P ENR for HA nodes to connect to const bootstrapNodeEnr = await aztecNode.getEncodedEnr(); @@ -256,7 +336,11 @@ describe('HA Full Setup', () => { }; const nodeService = await withLoggerBindings({ actor: `HA-${i}` }, async () => { - return await AztecNodeService.createAndSync(nodeConfig, { dateProvider }, { genesis }); + return await AztecNodeService.createAndSync( + nodeConfig, + { dateProvider }, + { genesis, dontStartSequencer: true }, + ); }); haNodeServices.push(nodeService); @@ -264,7 +348,7 @@ describe('HA Full Setup', () => { } logger.info(`All ${NODE_COUNT} HA peer nodes started and coordinating via PostgreSQL database`); - logger.info('Waiting for HA peer nodes to join the tx gossip mesh before deploying the test account'); + logger.info('Waiting for HA peer nodes to join the tx gossip mesh'); await retryUntil( async () => { const meshStates = await Promise.all( @@ -289,41 +373,38 @@ describe('HA Full Setup', () => { 1, ); - // Now deploy the account - blocks can be built by the HA nodes - logger.info('Deploying test account now that validators are running'); - const accountData = initialFundedAccounts[0]; - const accountManager = await wallet.createSchnorrAccount( - accountData.secret, - accountData.salt, - accountData.signingKey, - ); - const deployMethod = await accountManager.getDeployMethod(); - await deployMethod.send({ from: NO_FROM, wait: { waitForStatus: TxStatus.CHECKPOINTED } }); - ownerAddress = accountManager.address; - logger.info(`Test account deployed at ${ownerAddress}`); + logger.info(`Test account registered at ${ownerAddress}`); }); afterAll(async () => { - // Stop all HA peer nodes in parallel with a per-node deadline. A single stuck node can otherwise - // block the serial loop long enough to blow the jest hook timeout — e.g. a sequencer.stop() that - // awaits an L1 publish whose tx-timeout was computed on a test-warped clock and never fires. + // Stop all sequencers before tearing down the nodes: a sequencer stop awaits its in-flight + // iteration, which can spend tens of seconds finishing a vote or checkpoint publish on L1. + // Stops must be awaited fully — jest runs without forceExit, so a node abandoned mid-stop + // outlives the test environment and keeps the worker process alive until the CI job timeout. + // The dateProvider reset must wait until nodes are stopped: it rewinds the shared clock from + // chain time to wall time (minutes apart after the automine deploy burst), and any publisher + // deadline armed against the rewound clock would block shutdown until wall time catches up. if (haNodeServices) { - const STOP_DEADLINE_MS = 30_000; await Promise.allSettled( - haNodeServices.map((service, i) => { - logger.info(`Stopping HA peer node ${i}`); - return Promise.race([ - service.stop().catch(error => { - logger.error(`Failed to stop HA peer node ${i}: ${error}`); - }), - sleep(STOP_DEADLINE_MS).then(() => { - logger.error(`HA peer node ${i} stop did not return within ${STOP_DEADLINE_MS}ms; abandoning`); - }), - ]); + haNodeServices.map(async (service, i) => { + try { + await service.getSequencer()?.stop(); + } catch (error) { + logger.error(`Failed to stop sequencer of HA peer node ${i}: ${error}`); + } }), ); + await Promise.allSettled( + haNodeServices.map((_, i) => + stopHANode(i).catch(error => { + logger.error(`Failed to stop HA peer node ${i}: ${error}`); + }), + ), + ); } + dateProvider?.reset(); + // Cleanup HA keystore temp directories if (haKeystoreDirs) { for (let i = 0; i < haKeystoreDirs.length; i++) { @@ -368,20 +449,15 @@ describe('HA Full Setup', () => { it('should produce blocks with HA coordination and attestations', async () => { logger.info('Testing full HA setup: block production, attestations, and coordination'); - // Deploy a contract to trigger block building - const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); - logger.info(`Deploying contract from ${ownerAddress}`); - const { receipt } = await deployer.deploy([ownerAddress, 1], { salt: new Fr(BigInt(1)) }).send({ - from: ownerAddress, - wait: { waitForStatus: TxStatus.CHECKPOINTED }, - }); - - await waitForProven(aztecNode, receipt, { - provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, - }); + // Send a tx to trigger block building. The account and contract are funded/registered at genesis, + // so HA validators are the first block producers exercised by this suite. + logger.info(`Sending trigger tx from ${ownerAddress}`); + const txHash = await submitTriggerTx(wallet, testContract, ownerAddress); + await startHASequencers(); + const receipt = await waitForTriggerTx(aztecNode, txHash); expect(receipt.blockNumber).toBeDefined(); - logger.info(`Contract deployed in block ${receipt.blockNumber}`); + logger.info(`Trigger tx checkpointed in block ${receipt.blockNumber}`); // Get the block with attestations const [block] = await aztecNode.getBlocks(receipt.blockNumber!, 1, { @@ -493,11 +569,7 @@ describe('HA Full Setup', () => { // Send a transaction to trigger block building which will also trigger voting logger.info('Sending transaction to trigger block building...'); - const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); - const { receipt } = await deployer.deploy([ownerAddress, 42], { salt: Fr.random() }).send({ - from: ownerAddress, - wait: { waitForStatus: TxStatus.CHECKPOINTED }, - }); + const receipt = await sendTriggerTx(); expect(receipt.blockNumber).toBeDefined(); logger.info(`Transaction mined in block ${receipt.blockNumber}`); @@ -680,13 +752,9 @@ describe('HA Full Setup', () => { verifyNodeAttesters(i, i < 3 ? groupB : groupA, i < 3 ? 'group B (swapped)' : 'group A (swapped)'); } - const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); - const receipt = await deployer.deploy([ownerAddress, 201], { salt: new Fr(201) }).send({ - from: ownerAddress, - wait: { waitForStatus: TxStatus.CHECKPOINTED }, - }); - expect(receipt.receipt.blockNumber).toBeDefined(); - const [block] = await aztecNode.getBlocks(receipt.receipt.blockNumber!, 1, { + const receipt = await sendTriggerTx(); + expect(receipt.blockNumber).toBeDefined(); + const [block] = await aztecNode.getBlocks(receipt.blockNumber!, 1, { includeL1PublishInfo: true, includeAttestations: true, includeTransactions: true, @@ -695,7 +763,7 @@ describe('HA Full Setup', () => { const [cp] = await aztecNode.getCheckpoints(block!.checkpointNumber, 1, { includeAttestations: true }); const att = (cp.attestations ?? []).filter(a => !a.signature.isEmpty()); expect(att.length).toBeGreaterThanOrEqual(quorum); - logger.info(`Phase 2: block ${receipt.receipt.blockNumber}, ${att.length} attestations (quorum ${quorum})`); + logger.info(`Phase 2: block ${receipt.blockNumber}, ${att.length} attestations (quorum ${quorum})`); } finally { // Restore each node's saved initial keystore so subsequent tests see original state for (let i = 0; i < NODE_COUNT; i++) { @@ -727,11 +795,7 @@ describe('HA Full Setup', () => { logger.info(`\n=== Producing block ${i + 1}/${blockCount} ===`); logger.info(`Active nodes: ${haNodeServices.length - killedNodes.length}/${NODE_COUNT}`); - const deployer = new ContractDeployer(StatefulTestContractArtifact, wallet); - const { receipt } = await deployer.deploy([ownerAddress, i + 100], { salt: new Fr(BigInt(i + 100)) }).send({ - from: ownerAddress, - wait: { waitForStatus: TxStatus.CHECKPOINTED }, - }); + const receipt = await sendTriggerTx(); expect(receipt.blockNumber).toBeDefined(); @@ -764,20 +828,24 @@ describe('HA Full Setup', () => { blockProducers.set(i, blockProposalDuty.nodeId); logger.info(`Block ${receipt.blockNumber} produced by node ${blockProposalDuty.nodeId}`); - // Kill the node that produced this block, unless it's the last block - if (i < blockCount - 1) { - const producerNodeId = blockProposalDuty.nodeId; - const nodeIndexToKill = nodeIds.findIndex(nodeId => nodeId === producerNodeId); + const producerNodeId = blockProposalDuty.nodeId; + const producerNodeIndex = nodeIds.findIndex(nodeId => nodeId === producerNodeId); - if (nodeIndexToKill === -1) { - throw new Error(`Could not find active node with ID ${producerNodeId}`); - } + if (producerNodeIndex === -1) { + throw new Error(`Could not find active node with ID ${producerNodeId}`); + } + // Kill the node that produced this block, unless it's the last block + if (i < blockCount - 1) { logger.info(`Killing node ${producerNodeId} that produced this block`); - await haNodeServices[nodeIndexToKill].stop(); - killedNodes.push(nodeIndexToKill); + await stopHANode(producerNodeIndex); + killedNodes.push(producerNodeIndex); } else { - logger.info(`Last block produced.`); + // The final survivor is kept online for the slash-offense assertion below, but its sequencer + // is no longer needed. Stop it before running the remaining assertions so it cannot start a + // new empty checkpoint and then block service shutdown while awaiting a delayed L1 publish. + logger.info(`Last block produced; stopping sequencer for survivor ${producerNodeId}`); + await haNodeServices[producerNodeIndex].getSequencer()?.stop(); } logger.info(`Block ${i + 1}/${blockCount} completed. Killed nodes: ${killedNodes.length}/${NODE_COUNT}`); @@ -880,6 +948,8 @@ describe('HA Full Setup', () => { o => o.offenseType === OffenseType.DUPLICATE_ATTESTATION || o.offenseType === OffenseType.DUPLICATE_PROPOSAL, ); expect(equivocationOffenses).toEqual([]); + + await Promise.all(haNodeServices.map((_, nodeIndex) => stopHANode(nodeIndex))); }); describe('Clock Skew and Timezone Safety', () => { @@ -958,7 +1028,7 @@ describe('HA Full Setup', () => { } }); - it('should not delete recent duties when node clock is ahead (using cleanupOldDuties)', async () => { + it('should not delete recent duties via cleanupOldDuties when node clock is ahead', async () => { const spDb = new PostgresSlashingProtectionDatabase(mainPool); // Ensure clean slate for this test @@ -1020,7 +1090,7 @@ describe('HA Full Setup', () => { expect(result.rows.length).toBe(1); }); - it('should delete old duties based on DB time, not node time (using cleanupOldDuties)', async () => { + it('should delete old duties via cleanupOldDuties based on DB time, not node time', async () => { const spDb = new PostgresSlashingProtectionDatabase(mainPool); // Ensure clean slate for this test @@ -1089,7 +1159,7 @@ describe('HA Full Setup', () => { expect(result.rows.length).toBe(0); }); - it('should not delete recent stuck duties when node clock is ahead (using cleanupOwnStuckDuties)', async () => { + it('should not delete recent stuck duties via cleanupOwnStuckDuties when node clock is ahead', async () => { const spDb = new PostgresSlashingProtectionDatabase(mainPool); // Create a signing duty (stuck, not completed) using our actual method 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 01c5eac6464b..51548737aa57 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 @@ -3,6 +3,8 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import type { Wallet } from '@aztec/aztec.js/wallet'; import type { CheatCodes } from '@aztec/aztec/testing'; import { range } from '@aztec/foundation/array'; +import { CheckpointNumber } from '@aztec/foundation/branded-types'; +import { retryUntil } from '@aztec/foundation/retry'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import type { AztecNode, AztecNodeDebug } from '@aztec/stdlib/interfaces/client'; @@ -74,6 +76,27 @@ describe('e2e_automine_smoke', () => { expect(receipt.blockNumber).toBeGreaterThan(0); }); + it('prove advances the proven tip and clamps', async () => { + // Land a tx so there is a checkpointed checkpoint beyond the current proven tip to prove. + await contract.methods.emit_nullifier_public(BigInt(8000)).send({ from: owner }); + const checkpointed = (await aztecNode.getChainTips()).checkpointed.checkpoint.number; + expect(checkpointed).toBeGreaterThan(0); + + // No-arg proves up to the latest checkpointed checkpoint and returns it. + expect(await aztecNode.prove()).toBe(checkpointed); + + // The proven tip the archiver observes catches up after the synthetic settlement. + await retryUntil( + async () => (await aztecNode.getChainTips()).proven.checkpoint.number >= checkpointed, + 'proven tip advanced', + 30, + 0.5, + ); + + // A target beyond the checkpointed tip clamps; re-proving is an idempotent no-op. + expect(await aztecNode.prove(CheckpointNumber(checkpointed + 100))).toBe(checkpointed); + }); + it('mineBlock produces an empty checkpoint', async () => { const before = await aztecNode.getChainTips(); await aztecNode.mineBlock(); 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 b5b2645777e0..ec51fcd8ad85 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 @@ -6,7 +6,7 @@ import type { Logger } from '@aztec/aztec.js/log'; import { type AztecNode, waitForTx } from '@aztec/aztec.js/node'; import { TxStatus } from '@aztec/aztec.js/tx'; import { ContractInitializationStatus } from '@aztec/aztec.js/wallet'; -import { AnvilTestWatcher, CheatCodes } from '@aztec/aztec/testing'; +import { CheatCodes } from '@aztec/aztec/testing'; import { asyncMap } from '@aztec/foundation/async-map'; import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types'; import { times, unique } from '@aztec/foundation/collection'; @@ -44,7 +44,6 @@ describe('e2e_block_building', () => { let aztecNode: AztecNode; let aztecNodeAdmin: AztecNodeAdmin; let _sequencer: TestSequencerClient; - let watcher: AnvilTestWatcher; let teardown: () => Promise; afterEach(() => { @@ -81,8 +80,7 @@ describe('e2e_block_building', () => { fakeProcessingDelayPerTxMs: 0, minTxsPerBlock: 1, maxTxsPerBlock: undefined, // reset to default - enforceTimeTable: false, // reset to false (as it is in setup()) - blockDurationMs: undefined, // reset to single-block-per-slot mode + blockDurationMs: 3000, // reset to the PIPELINING_SETUP_OPTS fixture default (2 blocks/slot) }); // Clean up any mocks jest.restoreAllMocks(); @@ -97,10 +95,13 @@ describe('e2e_block_building', () => { // 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. it('processes txs until hitting timetable', async () => { - // Fixture defaults under pipelining: aztecSlotDuration=12s, ethereumSlotDuration=4s. With - // ethereumSlotDuration<8 the timing model uses checkpointInitializationTime=0.5s, - // checkpointAssembleTime=0.5s, p2pPropagationTime=0, minExecutionTime=1s. Picking a 2s sub-slot - // gives floor((12 - 0.5 - (0.5 + 2)) / 2) = 4 sub-slots per slot. + // The timetable is always enforced. Fixture defaults under pipelining: aztecSlotDuration=12s, + // ethereumSlotDuration=4s. With ethereumSlotDuration<8 the timing model normalizes to + // checkpointInitializationTime=0.5s, checkpointAssembleTime=0.5s, p2pPropagationTime=0, + // minExecutionTime=1s. We override blockDurationMs to a 2s sub-slot for this test, giving + // maxBlocks = floor((12 - 0.5 - (0.5 + 0 + 2)) / 2) = floor(9/2) = 4 sub-slots per slot — more + // sub-slots than the fixture default (3s -> 2 blocks/slot) so the cut-across-blocks invariant + // is easier to assert. Sub-slot build deadlines fall at 0.5 + k*2s into the slot. const BLOCK_DURATION_MS = 2000; // Fake delay per tx, sized so ~3 txs fit in a 2s sub-slot before the builder cuts at the deadline. const FAKE_DELAY_PER_TX_MS = 500; @@ -122,7 +123,6 @@ describe('e2e_block_building', () => { fakeProcessingDelayPerTxMs: FAKE_DELAY_PER_TX_MS, minTxsPerBlock: 1, maxTxsPerBlock: TX_COUNT, // intentionally large; we want to flex the sub-slot deadline, not this cap - enforceTimeTable: true, blockDurationMs: BLOCK_DURATION_MS, }); @@ -544,8 +544,12 @@ describe('e2e_block_building', () => { }, ); - logger.info('Updating txs per block to 4'); - await aztecNodeAdmin.setConfig({ minTxsPerBlock: 4, maxTxsPerBlock: 4 }); + // Cap blocks at 4 txs so building spans several blocks while the 24 sends below simulate concurrently. + // minTxsPerBlock must stay at 1: with the timetable always enforced, a leftover batch smaller than + // minTxsPerBlock can never form a block (the sub-slot deadline cuts and discards it every slot), so a + // higher minimum livelocks the test if the tx count doesn't divide evenly into blocks. + logger.info('Updating max txs per block to 4'); + await aztecNodeAdmin.setConfig({ minTxsPerBlock: 1, maxTxsPerBlock: 4 }); logger.info('Spamming the network with public txs'); const txs = []; @@ -629,7 +633,6 @@ describe('e2e_block_building', () => { logger, wallet, cheatCodes, - watcher, accounts: [ownerAddress], } = await setup(1, { ...PIPELINING_SETUP_OPTS, minTxsPerBlock: 1 })); @@ -640,13 +643,11 @@ describe('e2e_block_building', () => { await cheatCodes.rollup.advanceToNextEpoch(); // Mark all blocks up to the current pending tip as proven so the contract-deployment block - // is anchored against a proven checkpoint. The e2e fixture's AnvilTestWatcher does NOT - // auto-prove under interval mining (only under automining), so we must drive proven manually. + // is anchored against a proven checkpoint. Nothing auto-proves under the e2e fixture's L1 + // interval mining, so we drive proven manually here (and again inside each test). await cheatCodes.rollup.markAsProven(); const bn = await aztecNode.getBlockNumber(); await retryUntil(async () => (await aztecNode.getBlockNumber('proven')) >= bn, 'wait-proven', 60, 1); - - watcher.setIsMarkingAsProven(false); }); afterEach(() => teardown()); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index 8b1555a732f3..80b9d3abf716 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -67,10 +67,10 @@ export class CrossChainMessagingTest { /** * Background loop that marks each completed epoch as proven on L1. Started in `applyBaseSetup` * when the test runs without a real prover node, because the e2e fixture uses L1 interval mining - * and the AnvilTestWatcher's auto-prove loop only runs under L1 automine. Without this, L1's - * `aztecProofSubmissionEpochs` window expires mid-test and triggers a chain prune that drops - * in-flight wallet txs. Tests that intentionally pause proving (e.g. inbox drift tests) can - * stop it via `await t.epochTestSettler?.stop()`. + * and nothing marks blocks proven automatically. Without this, L1's `aztecProofSubmissionEpochs` + * window expires mid-test and triggers a chain prune that drops in-flight wallet txs. Tests that + * intentionally pause proving (e.g. inbox drift tests) can stop it via + * `await t.epochTestSettler?.stop()`. */ epochTestSettler?: EpochTestSettler; @@ -158,14 +158,11 @@ export class CrossChainMessagingTest { this.deployL1ContractsValues = this.context.deployL1ContractsValues; this.aztecNodeAdmin = this.context.aztecNodeService; - if (this.requireEpochProven) { - // Turn off the watcher to prevent it from keep marking blocks as proven. - this.context.watcher.setIsMarkingAsProven(false); - } else { + if (!this.requireEpochProven) { // When no real prover is running, the L1 proof window (aztecProofSubmissionEpochs) would - // otherwise expire mid-test and trigger a chain prune. The AnvilTestWatcher's auto-prove - // loop is dormant under L1 interval mining (it gates on `isAutoMining`), so start an - // EpochTestSettler to mark each completed epoch as proven on L1. + // otherwise expire mid-test and trigger a chain prune. The e2e fixture runs L1 on interval + // mining and nothing marks blocks proven automatically, so start an EpochTestSettler to mark + // each completed epoch as proven on L1. this.epochTestSettler = new EpochTestSettler( this.context.ethCheatCodes, this.context.deployL1ContractsValues.l1ContractAddresses.rollupAddress, 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 7a182f2d4c6d..9d00b838a449 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 @@ -6,7 +6,7 @@ import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import type { AztecNode } from '@aztec/aztec.js/node'; import { TxExecutionResult } from '@aztec/aztec.js/tx'; import type { Wallet } from '@aztec/aztec.js/wallet'; -import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { timesAsync } from '@aztec/foundation/collection'; import { retryUntil } from '@aztec/foundation/retry'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; @@ -30,7 +30,22 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { let user1Address: AztecAddress; let testContract: TestContract; + // Whether explicit mark-as-proven calls are honored. The inbox-drift scenario flips this to + // false to let the proposed chain drift and prune. + let markProvenEnabled = true; + + // Marks the current pending tip proven on L1, gated by `markProvenEnabled`. The e2e fixture runs + // L1 on interval mining and nothing marks blocks proven automatically, so without these explicit + // calls L1's `aztecProofSubmissionEpochs` window expires mid-test and prunes in-flight wallet txs. + const markAsProven = async () => { + if (!markProvenEnabled) { + return; + } + await t.cheatCodes.rollup.markAsProven(); + }; + beforeEach(async () => { + markProvenEnabled = true; t = new CrossChainMessagingTest( 'l1_to_l2', // PIPELINING_SETUP_OPTS sets minTxsPerBlock=0; this test needs minTxsPerBlock=1 because it @@ -68,10 +83,9 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { if (newBlock === block) { throw new Error(`Failed to advance block ${block}`); } - // Under interval mining `AnvilTestWatcher.markAsProven` does not auto-fire; without an explicit - // prove call here, L1's `aztecProofSubmissionEpochs=2` window (96s with pipelined 12s slots) - // expires mid-test and triggers a chain prune that drops in-flight wallet txs. - await t.context.watcher.markAsProven(); + // Keep the proof window from expiring mid-test (see `markAsProven` above). No-op once the drift + // scenario disables proving. + await markAsProven(); return newBlock; }; @@ -228,12 +242,9 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { // an auto-prover racing it. await t.epochTestSettler?.stop(); - // Reset the L1 proof window by marking the current pending tip as proven. The e2e fixture - // runs L1 on interval mining, so the watcher's auto-prove loop never starts (it gates on - // `isAutoMining`). That means L1's prune deadline has been anchored to chain genesis the - // whole setup, and would otherwise fire mid-test before we finish mining the 4 drift - // checkpoints below. - await t.context.watcher.markAsProven(); + // Reset the L1 proof window by marking the current pending tip as proven, so L1's prune + // deadline doesn't fire mid-test before we finish mining the 4 drift checkpoints below. + await markAsProven(); // Stop proving const lastProven = await aztecNode.getBlockNumber(); @@ -243,7 +254,7 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { onlyCheckpointed: true, }); log.warn(`Stopping proof submission at checkpoint ${checkpointedProvenBlock.checkpointNumber} to allow drift`); - t.context.watcher.setIsMarkingAsProven(false); + markProvenEnabled = false; // Mine several checkpoints to ensure drift log.warn(`Mining blocks to allow drift`); @@ -271,6 +282,10 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { 'wait for prune', 180, ); + // The drift condition has been established. Re-enable explicit proving so the catch-up blocks + // below are not pruned a second time before the message checkpoint becomes ready. + markProvenEnabled = true; + await markAsProven(); // Check that there is no witness yet expect(await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash)).toBeUndefined(); @@ -292,17 +307,16 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { // If it fails we check that the block doesn't contain the message const { receipt } = await consume().send({ from: user1Address, wait: { dontThrowOnRevert: true } }); if (receipt.executionResult === TxExecutionResult.SUCCESS) { - // The block the transaction included should be for the message checkpoint number - // and be the first block in the checkpoint + // The consume tx must not succeed before the message checkpoint. It can land in a later + // checkpoint if the node catches up between the readiness poll and the tx being built. const block = await aztecNode.getBlock(receipt.blockNumber!); expect(block).toBeDefined(); - expect(block!.checkpointNumber).toEqual(msgCheckpointNumber); - expect(block!.indexWithinCheckpoint).toEqual(IndexWithinCheckpoint.ZERO); + expect(block!.checkpointNumber).toBeGreaterThanOrEqual(msgCheckpointNumber); } else { expect(receipt.executionResult).toEqual(TxExecutionResult.REVERTED); } } - await t.context.watcher.markAsProven(); + await markAsProven(); }); // Verify the membership witness is available for creating the tx (private-land only) 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 36a8864f0f5b..3a54c4bd9e90 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 @@ -325,12 +325,11 @@ describe('e2e_cross_chain_messaging l2_to_l1', () => { const contents = [Fr.random(), Fr.random()]; const messages = contents.map(content => makeL2ToL1Message(recipient, content)); - // Enable multiple-blocks-per-checkpoint: enforce the timetable so the sequencer splits the slot - // into per-block sub-slots, cap each block at a single tx, and require (and accept at most) two - // blocks before publishing the checkpoint. With the two txs below this yields one checkpoint - // holding two single-tx blocks. + // Enable multiple-blocks-per-checkpoint: the always-enforced timetable splits the slot into + // per-block sub-slots (blockDurationMs=2000), cap each block at a single tx, and require (and + // accept at most) two blocks before publishing the checkpoint. With the two txs below this + // yields one checkpoint holding two single-tx blocks. await aztecNodeAdmin.setConfig({ - enforceTimeTable: true, blockDurationMs: 2000, minTxsPerBlock: 1, maxTxsPerBlock: 1, 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 17137e8a0c74..1a45dfe19118 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 @@ -65,11 +65,9 @@ describe('e2e_epochs/epochs_equivocation', () => { initialValidators: validators, inboxLag: 2, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: false, aztecEpochDuration: 4, aztecProofSubmissionEpochs: 1024, - enforceTimeTable: true, ethereumSlotDuration: 6, aztecSlotDuration: 36, blockDurationMs: 8000, 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 a2240b7b4784..6d4acc9d08a5 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 @@ -52,13 +52,12 @@ describe('e2e_epochs/epochs_first_slot', () => { return { attester, withdrawer: attester, privateKey, bn254SecretKey: new SecretValue(Fr.random().toBigInt()) }; }); - // Setup context with the given set of validators, no reorgs, mocked gossip sub network, and no anvil test watcher. + // Setup context with the given set of validators, no reorgs, and a mocked gossip sub network. // We expect 4 blocks per checkpoint with this config test = await EpochsTestContext.setup({ numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecProofSubmissionEpochs: 1024, aztecEpochDuration: 32, aztecSlotDurationInL1Slots: 3, @@ -66,7 +65,6 @@ describe('e2e_epochs/epochs_first_slot', () => { blockDurationMs: 6000, startProverNode: false, aztecTargetCommitteeSize: COMMITTEE_SIZE, - enforceTimeTable: true, minTxsPerBlock: 1, maxTxsPerBlock: 1, attestationPropagationTime: 0.5, 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 94a0b4936ef7..6f2d1c4e31c6 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 @@ -92,12 +92,10 @@ describe('e2e_epochs/epochs_ha_checkpoint_handoff', () => { test = await EpochsTestContext.setup({ initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: false, skipInitialSequencer: true, aztecEpochDuration: 8, aztecProofSubmissionEpochs: 1024, - enforceTimeTable: true, ethereumSlotDuration: 6, aztecSlotDuration: 36, blockDurationMs: 8000, 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 79c3ccc4df28..f4a2ad646cb9 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 @@ -58,9 +58,7 @@ describe('e2e_epochs/epochs_ha_sync', () => { numberOfAccounts: 1, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecEpochDuration: 4, - enforceTimeTable: true, ethereumSlotDuration: 4, aztecSlotDuration: 36, blockDurationMs: 8000, 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 5444096bee86..c6ece3776d92 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 @@ -88,10 +88,8 @@ describe('e2e_epochs/epochs_high_tps_block_building', () => { numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecProofSubmissionEpochs: 1024, startProverNode: false, - enforceTimeTable: true, ethereumSlotDuration: L1_BLOCK_TIME_S, aztecSlotDuration: L2_SLOT_DURATION_S, blockDurationMs: BLOCK_DURATION_MS, 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 d613036b35c1..10c6b9182b76 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 @@ -60,17 +60,15 @@ describe('e2e_epochs/epochs_invalidate_block', () => { return { attester, withdrawer: attester, privateKey, bn254SecretKey: new SecretValue(Fr.random().toBigInt()) }; }); - // Setup context with the given set of validators, mocked gossip sub network, and no anvil test watcher. + // Setup context with the given set of validators and a mocked gossip sub network. // Uses multiple-blocks-per-slot timing configuration. test = await EpochsTestContext.setup({ ethereumSlotDuration: 8, aztecSlotDuration: 32, blockDurationMs: 6000, - enforceTimeTable: true, numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecProofSubmissionEpochs: 1024, startProverNode: false, aztecTargetCommitteeSize: VALIDATOR_COUNT, 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 0fb6d5143894..b3566028d19f 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 @@ -72,7 +72,6 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { blockDurationMs: 8000, minTxsPerBlock: 0, maxTxsPerBlock: 1, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1, // Use 32 slots/epoch (matching real Ethereum mainnet) anvilSlotsInAnEpoch: 32, 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 03df1e414ad5..fda370a8dae9 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 @@ -90,13 +90,11 @@ describe('e2e_epochs/epochs_mbps', () => { numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: true, // Mirrors the pipeline-MBPS sibling: more blocks per slot needs a larger per-block gas // allocation multiplier so each block can fit non-trivial txs. perBlockAllocationMultiplier: 8, aztecEpochDuration: 4, - enforceTimeTable: true, // L1 slot duration - mirrors the pipeline-MBPS test for headroom on the parent's L1 tx ethereumSlotDuration: 12, // L2 slot duration - should fit several blocks (5.5s each) with pipelining overhead 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 583d4738c9f1..a8fa73888dac 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 @@ -73,11 +73,9 @@ describe('e2e_epochs/epochs_mbps_pipeline', () => { initialValidators: validators, mockGossipSubNetwork: true, mockGossipSubNetworkLatency: 500, // adverse network conditions - disableAnvilTestWatcher: true, startProverNode: true, perBlockAllocationMultiplier: 8, aztecEpochDuration: 4, - enforceTimeTable: true, ethereumSlotDuration: 12, aztecSlotDuration: 72, blockDurationMs: 5500, 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 966ac7316249..3a7b8e7f16d6 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 @@ -96,10 +96,8 @@ describe('e2e_epochs/epochs_mbps_redistribution', () => { initialValidators: validators, inboxLag: 2, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: true, aztecEpochDuration: 4, - enforceTimeTable: true, ethereumSlotDuration: 4, aztecSlotDuration: 36, blockDurationMs: 8000, 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 e86c99c2d0dd..fb5aa4afd1ce 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 @@ -73,11 +73,9 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { initialValidators: validators, inboxLag: 2, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: false, aztecEpochDuration: 4, aztecProofSubmissionEpochs: 1024, - enforceTimeTable: true, ethereumSlotDuration: 6, aztecSlotDuration: 36, blockDurationMs: 8000, 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 c6bca7b4cd3e..d15a5a300cdf 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 @@ -87,8 +87,6 @@ describe('e2e_epochs/epochs_missed_l1_slot', () => { aztecSlotDurationInL1Slots: L1_SLOTS_PER_L2_SLOT, startProverNode: false, aztecProofSubmissionEpochs: 1024, - disableAnvilTestWatcher: true, - enforceTimeTable: true, inboxLag: 2, // Required for the proposer's own broadcasts to route through the local // proposal handler (the dummy p2p service drops them). Without this, the 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 27c0e2dd2e97..6a4335c5988b 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 @@ -219,7 +219,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, inboxLag: 2, @@ -363,7 +362,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, }); @@ -470,7 +468,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, }); @@ -555,7 +552,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, inboxLag: 2, @@ -643,7 +639,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, }); @@ -777,7 +772,6 @@ describe('e2e_epochs/epochs_optimistic_proving', () => { aztecSlotDuration: 36, blockDurationMs: 8000, minTxsPerBlock: 0, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1000, anvilSlotsInAnEpoch: 32, }); 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 ae2c6ea45bf6..dbf1a088b277 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 @@ -60,11 +60,9 @@ describe('e2e_epochs/epochs_orphan_block_prune', () => { initialValidators: validators, inboxLag: 2, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, startProverNode: false, aztecEpochDuration: 4, aztecProofSubmissionEpochs: 1024, - enforceTimeTable: true, ethereumSlotDuration: 6, aztecSlotDuration: 36, blockDurationMs: 8000, 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 54043d2358eb..60116fb31795 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 @@ -51,6 +51,10 @@ describe('e2e_epochs/epochs_partial_proof_multi_root', () => { test = await EpochsTestContext.setup({ numberOfAccounts: 1, minTxsPerBlock: 1, + // With the enforced timetable this setup can have 5 blocks per checkpoint. The default + // fallback DA gas for the TestContract deploy is based on a 4-block checkpoint, so give the + // first block enough of the checkpoint DA budget to include the deploy tx. + perBlockAllocationMultiplier: 1.3, // Long epoch so 4 well-spaced checkpoints comfortably fit before the boundary. aztecEpochDuration: 1000, // Don't let the real prover land a partial proof under us. We drive Outbox state via the @@ -58,7 +62,6 @@ describe('e2e_epochs/epochs_partial_proof_multi_root', () => { // submission window. aztecProofSubmissionEpochs: 1024, startProverNode: false, - disableAnvilTestWatcher: true, }); ({ logger } = test); node = test.context.aztecNode; 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 82a2b3e9f8ea..d0477f967d68 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 @@ -49,13 +49,11 @@ describe('e2e_epochs/epochs_proof_at_boundary', () => { numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecProofSubmissionEpochs: 1024, aztecSlotDurationInL1Slots: 3, ethereumSlotDuration: 12, blockDurationMs: 6000, startProverNode: false, - enforceTimeTable: true, skipInitialSequencer: true, inboxLag: 2, ...overrides, 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 379091db6b21..8da131ebd02c 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 @@ -44,7 +44,6 @@ describe('e2e_epochs/epochs_proof_fails', () => { aztecSlotDurationInL1Slots: 2, blockDurationMs: 3000, // 3s blocks → 2 blocks per checkpoint under pipelining cancelTxOnTimeout: false, - enforceTimeTable: true, inboxLag: 2, }); ({ context, l1Client, rollup, constants, logger, monitor } = test); 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 946bb268e3f9..1dc83cb637c5 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 @@ -31,7 +31,6 @@ describe('e2e_epochs/epochs_proof_public_cross_chain', () => { test = await EpochsTestContext.setup({ numberOfAccounts: 1, minTxsPerBlock: 1, - disableAnvilTestWatcher: true, sequencerPublisherAllowInvalidStates: true, }); ({ context, logger } = test); 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 0630fb91d6b3..7a3f203021d2 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 @@ -51,13 +51,11 @@ describe('e2e_epochs/epochs_simple_block_building', () => { numberOfAccounts: 0, initialValidators: validators, mockGossipSubNetwork: true, - disableAnvilTestWatcher: true, aztecProofSubmissionEpochs: 1024, aztecSlotDurationInL1Slots: 3, ethereumSlotDuration: 12, blockDurationMs: 6000, startProverNode: false, - enforceTimeTable: true, skipInitialSequencer: true, inboxLag: 2, }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts index b172be919708..aca1f9538b01 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts @@ -2,6 +2,7 @@ import type { InitialAccountData } from '@aztec/accounts/testing'; import type { Archiver } from '@aztec/archiver'; import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; import { getAccountContractAddress } from '@aztec/aztec.js/account'; +import type { AztecAddress } from '@aztec/aztec.js/addresses'; import { getTimestampRangeForEpoch } from '@aztec/aztec.js/block'; import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; import { Fr } from '@aztec/aztec.js/fields'; @@ -233,7 +234,7 @@ export class EpochsTestContext { * Registers a SchnorrHardcodedKeyAccountContract in PXE. The account must have been funded * at genesis (via getHardcodedAccountData). No on-chain deployment or block mining needed. */ - public async registerHardcodedAccount(accountData: InitialAccountData) { + public async registerHardcodedAccount(accountData: InitialAccountData): Promise { const contract = new SchnorrHardcodedKeyAccountContract(); const wallet = this.context.wallet; const accountManager = await (wallet as TestWallet).createAccount({ 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 a59840840152..8aa4e2b652a0 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 @@ -26,11 +26,18 @@ describe('FeeAssetPriceOracle E2E', () => { // Beware, if you use "mainnet" here it will be completely broken due to blobs... const chain = foundry; + // Convergence is gated by L1 checkpoint publication. Under real sequencer timing, a parent checkpoint can land late + // and cause pipelined work to be rebuilt, so leave room for multiple checkpoint publications. + const PRICE_CONVERGENCE_TIMEOUT_SECONDS = 240; + const PRICE_CONVERGENCE_POLL_INTERVAL_SECONDS = 1; beforeAll(async () => { logger = getLogger(); - const anvilResult = await startAnvil({ chainId: chain.id }); + const anvilResult = await startAnvil({ + chainId: chain.id, + l1BlockTime: PIPELINING_SETUP_OPTS.ethereumSlotDuration, + }); anvil = anvilResult.anvil; const rpcUrl = anvilResult.rpcUrl; @@ -89,14 +96,14 @@ describe('FeeAssetPriceOracle E2E', () => { return diffInBps(currentPrice, targetOraclePrice) == 0n; }, 'price convergence toward oracle', - 120, // timeout in seconds - 5, // check interval in seconds + PRICE_CONVERGENCE_TIMEOUT_SECONDS, + PRICE_CONVERGENCE_POLL_INTERVAL_SECONDS, ); const priceAfterFirstAlignment = await rollup.getEthPerFeeAsset(); const targetOraclePrice2 = (BigInt(priceAfterFirstAlignment) * 995n) / 1000n; await mockStateView.setEthPerFeeAsset(targetOraclePrice2); - logger.info(`Set uniswap price to ${targetOraclePrice}`); + logger.info(`Set uniswap price to ${targetOraclePrice2}`); await retryUntil( async () => { @@ -105,8 +112,8 @@ describe('FeeAssetPriceOracle E2E', () => { return diffInBps(currentPrice, targetOraclePrice2) == 0n; }, 'price convergence toward oracle', - 120, // timeout in seconds - 5, // check interval in seconds + PRICE_CONVERGENCE_TIMEOUT_SECONDS, + PRICE_CONVERGENCE_POLL_INTERVAL_SECONDS, ); const finalPrice = 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 b05bf4ce0edf..ca103af08bc5 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 @@ -13,7 +13,7 @@ import type { FPCContract } from '@aztec/noir-contracts.js/FPC'; import { SchnorrAccountContract as SchnorrAccountContractInterface } from '@aztec/noir-contracts.js/SchnorrAccount'; import type { TokenContract as BananaCoin } from '@aztec/noir-contracts.js/Token'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasSettings } from '@aztec/stdlib/gas'; import { jest } from '@jest/globals'; @@ -119,7 +119,8 @@ describe('e2e_fees account_init', () => { // The private fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay const maxFeesPerGas = await getPaddedMaxFeesPerGas(aztecNode); - const gasSettings = GasSettings.fallback({ maxFeesPerGas }); + const gasLimits = Gas.from((await aztecNode.getNodeInfo()).txsLimits.gas); + const gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); const paymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, @@ -147,7 +148,8 @@ describe('e2e_fees account_init', () => { // The public fee paying method assembled on the app side requires knowledge of the maximum // fee the user is willing to pay const maxFeesPerGas = await getPaddedMaxFeesPerGas(aztecNode); - const gasSettings = GasSettings.fallback({ maxFeesPerGas }); + const gasLimits = Gas.from((await aztecNode.getNodeInfo()).txsLimits.gas); + const gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); const paymentMethod = new PublicFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, @@ -202,7 +204,8 @@ describe('e2e_fees account_init', () => { // bob can now use his wallet for sending txs const maxFeesPerGas = await getPaddedMaxFeesPerGas(aztecNode); - const gasSettings = GasSettings.fallback({ maxFeesPerGas }); + const gasLimits = Gas.from((await aztecNode.getNodeInfo()).txsLimits.gas); + const gasSettings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); const bobPaymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); await bananaCoin.methods .transfer_in_public(bobsAddress, aliceAddress, 0n, 0n) 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 210d84db9ffd..36df2220b67f 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 @@ -47,12 +47,9 @@ describe('e2e_fees failures', () => { await ensureAuthRegistryPublished(wallet, aliceAddress); aztecNode = t.aztecNode; - // Prove up until the current state by just marking it as proven. - // Then turn off the watcher to prevent it from keep proving - await t.context.watcher.trigger(); + // Prove up until the current state by advancing the epoch and waiting for the prover node. await t.cheatCodes.rollup.advanceToNextEpoch(); await t.catchUpProvenChain(); - t.setIsMarkingAsProven(false); }); afterAll(async () => { @@ -93,7 +90,6 @@ 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. - await t.context.watcher.trigger(); await t.cheatCodes.rollup.advanceToNextEpoch(); await t.catchUpProvenChain(); @@ -116,7 +112,6 @@ describe('e2e_fees failures', () => { // @note There is a potential race condition here if other tests send transactions that get into the same // epoch and thereby pays out fees at the same time (when proven). - await t.context.watcher.trigger(); await t.cheatCodes.rollup.advanceToNextEpoch(); const provenTimeout = (t.context.config.aztecProofSubmissionEpochs + 1) * @@ -349,7 +344,6 @@ describe('e2e_fees failures', () => { ); // Prove the block containing the teardown-reverted tx (revert_code = 2). - await t.context.watcher.trigger(); await t.cheatCodes.rollup.advanceToNextEpoch(); const provenTimeout = (t.context.config.aztecProofSubmissionEpochs + 1) * @@ -375,7 +369,6 @@ describe('e2e_fees failures', () => { expect(receipt.executionResult).toBe(TxExecutionResult.REVERTED); expect(receipt.transactionFee).toBeGreaterThan(0n); - await t.context.watcher.trigger(); await t.cheatCodes.rollup.advanceToNextEpoch(); const provenTimeout = (t.context.config.aztecProofSubmissionEpochs + 1) * diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index e722ce0ec332..d51bbf1ad861 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -18,7 +18,7 @@ import { TokenContract as BananaCoin } from '@aztec/noir-contracts.js/Token'; import { CounterContract } from '@aztec/noir-test-contracts.js/Counter'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; -import { GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasSettings } from '@aztec/stdlib/gas'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { getContract } from 'viem'; @@ -132,10 +132,6 @@ export class FeesTest { await teardown(this.context); } - setIsMarkingAsProven(b: boolean) { - this.context.watcher.setIsMarkingAsProven(b); - } - async catchUpProvenChain() { const bn = await this.aztecNode.getBlockNumber(); while ((await this.aztecNode.getBlockNumber('proven')) < bn) { @@ -204,6 +200,7 @@ export class FeesTest { this.aztecNode = this.context.aztecNodeService; this.aztecNodeAdmin = this.context.aztecNodeService; this.gasSettings = GasSettings.fallback({ + gasLimits: Gas.from((await this.aztecNode.getNodeInfo()).txsLimits.gas), maxFeesPerGas: await getPaddedMaxFeesPerGas(this.aztecNode), }); this.cheatCodes = this.context.cheatCodes; 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 5a629951347b..0e38620091da 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 @@ -14,7 +14,9 @@ import { Gas, GasFees, GasSettings, + type GasUsed, } from '@aztec/stdlib/gas'; +import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; import { jest } from '@jest/globals'; import { inspect } from 'util'; @@ -73,11 +75,18 @@ describe('e2e_fees gas_estimation', () => { ({ wallet, aliceAddress, bobAddress, bananaCoin, bananaFPC, gasSettings, logger, aztecNode } = t); }); + // Derives declared gas limits from simulated usage with zero padding, mirroring what the old + // `estimateGas: true, estimatedGasPadding: 0` flow produced: `gasLimits == manaUsed`. + const estimateGasLimits = async (gasUsed: GasUsed): Promise> => { + const { txsLimits } = await aztecNode.getNodeInfo(); + return getGasLimits(gasUsed, Gas.from(txsLimits.gas), 0); + }; + beforeEach(async () => { // Pad max fees per gas to absorb pipelined fee-asset price evolution between snapshot and // submission. The assertions below compare `transactionFee` (manaUsed * block.gasFees) against // `estimatedGas.gasLimits.computeFee(block.gasFees)`, so they only require `gasLimits == manaUsed` - // (guaranteed by `estimatedGasPadding: 0`); they do not require `maxFeesPerGas == block.gasFees`. + // (guaranteed by zero padding); they do not require `maxFeesPerGas == block.gasFees`. const paddedMaxFees = await getPaddedMaxFeesPerGas(aztecNode); gasSettings = GasSettings.from({ ...gasSettings, @@ -116,9 +125,10 @@ describe('e2e_fees gas_estimation', () => { it('estimates gas with Fee Juice payment method', async () => { const sim = await makeTransferRequest().simulate({ from: aliceAddress, - fee: { gasSettings, estimateGas: true, estimatedGasPadding: 0 }, + fee: { gasSettings }, + includeMetadata: true, }); - const estimatedGas = sim.estimatedGas!; + const estimatedGas = await estimateGasLimits(sim.gasUsed!); logGasEstimate(estimatedGas); const sequencer = t.context.sequencer!.getSequencer(); @@ -160,9 +170,10 @@ describe('e2e_fees gas_estimation', () => { const sim2 = await makeTransferRequest().simulate({ from: aliceAddress, - fee: { paymentMethod, estimatedGasPadding: 0, estimateGas: true }, + fee: { paymentMethod }, + includeMetadata: true, }); - const estimatedGas = sim2.estimatedGas!; + const estimatedGas = await estimateGasLimits(sim2.gasUsed!); logGasEstimate(estimatedGas); const [withEstimate, withoutEstimate] = await sendTransfers(estimatedGas, paymentMethod); @@ -202,12 +213,9 @@ describe('e2e_fees gas_estimation', () => { const sim3 = await deployMethod().simulate({ from: aliceAddress, skipClassPublication: true, - fee: { - estimateGas: true, - estimatedGasPadding: 0, - }, + includeMetadata: true, }); - const estimatedGas = sim3.estimatedGas!; + const estimatedGas = await estimateGasLimits(sim3.gasUsed!); logGasEstimate(estimatedGas); const [{ receipt: withEstimate }, { receipt: withoutEstimate }] = await Promise.all([ 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 15b37f516b5d..7eb6043ee19c 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 @@ -40,11 +40,9 @@ describe('e2e_fees private_payment', () => { await t.applyFundAliceWithBananas(); ({ wallet, aliceAddress, bobAddress, sequencerAddress, bananaCoin, bananaFPC, gasSettings, aztecNode } = t); - // Prove up until the current state by just marking it as proven. - // Then turn off the watcher to prevent it from keep proving + // Prove up until the current state by advancing the epoch and waiting for the prover node. await t.cheatCodes.rollup.advanceToNextEpoch(); await t.catchUpProvenChain(); - t.setIsMarkingAsProven(false); }); afterAll(async () => { 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 1543890e7c59..effd18706243 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 @@ -13,9 +13,9 @@ describe('e2e_genesis_timestamp', () => { beforeEach(async () => { // Skip account deployment and configure PXE to sync its anchor only to proven blocks so its - // anchor lags behind proposed blocks. Under AUTOMINE_E2E_OPTS the AnvilTestWatcher is disabled - // and the AutomineSequencer never marks blocks as proven on its own, so without a prover node - // the proven tip stays at genesis for the duration of the test. + // anchor lags behind proposed blocks. Under AUTOMINE_E2E_OPTS the AutomineSequencer never marks + // blocks as proven on its own, so without a prover node the proven tip stays at genesis for the + // duration of the test. context = await setup(0, { ...AUTOMINE_E2E_OPTS, skipAccountDeployment: true }, { syncChainTip: 'proven' }); }); 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 e89ac4a3c436..4e25df6d934e 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 @@ -259,7 +259,7 @@ describe('Kernelless simulation', () => { from: swapperAddress, includeMetadata: true, }); - const swapKernellessGas = kernellessResult.estimatedGas!; + const swapKernellessGas = kernellessResult.gasUsed!; const swapAuthwit = await wallet.createAuthWit(swapperAddress, { caller: amm.address, @@ -272,15 +272,13 @@ describe('Kernelless simulation', () => { includeMetadata: true, authWitnesses: [swapAuthwit], }); - const swapWithKernelsGas = withKernelsResult.estimatedGas!; + const swapWithKernelsGas = withKernelsResult.gasUsed!; - logger.info(`Kernelless gas: L2=${swapKernellessGas.gasLimits.l2Gas} DA=${swapKernellessGas.gasLimits.daGas}`); - logger.info( - `With kernels gas: L2=${swapWithKernelsGas.gasLimits.l2Gas} DA=${swapWithKernelsGas.gasLimits.daGas}`, - ); + logger.info(`Kernelless gas: L2=${swapKernellessGas.totalGas.l2Gas} DA=${swapKernellessGas.totalGas.daGas}`); + logger.info(`With kernels gas: L2=${swapWithKernelsGas.totalGas.l2Gas} DA=${swapWithKernelsGas.totalGas.daGas}`); - expect(swapKernellessGas.gasLimits.daGas).toEqual(swapWithKernelsGas.gasLimits.daGas); - expect(swapKernellessGas.gasLimits.l2Gas).toEqual(swapWithKernelsGas.gasLimits.l2Gas); + expect(swapKernellessGas.totalGas.daGas).toEqual(swapWithKernelsGas.totalGas.daGas); + expect(swapKernellessGas.totalGas.l2Gas).toEqual(swapWithKernelsGas.totalGas.l2Gas); expect(simulateTxSpy).toHaveBeenCalledTimes(2); const kernellessTxResult = await (simulateTxSpy.mock.results[0].value as ReturnType); @@ -320,7 +318,7 @@ describe('Kernelless simulation', () => { from: adminAddress, includeMetadata: true, }) - ).estimatedGas!; + ).gasUsed!; wallet.setSimulationMode('full'); const withKernelsGas = ( @@ -328,13 +326,13 @@ describe('Kernelless simulation', () => { from: adminAddress, includeMetadata: true, }) - ).estimatedGas!; + ).gasUsed!; - logger.info(`Kernelless gas: L2=${kernellessGas.gasLimits.l2Gas} DA=${kernellessGas.gasLimits.daGas}`); - logger.info(`With kernels gas: L2=${withKernelsGas.gasLimits.l2Gas} DA=${withKernelsGas.gasLimits.daGas}`); + logger.info(`Kernelless gas: L2=${kernellessGas.totalGas.l2Gas} DA=${kernellessGas.totalGas.daGas}`); + logger.info(`With kernels gas: L2=${withKernelsGas.totalGas.l2Gas} DA=${withKernelsGas.totalGas.daGas}`); - expect(kernellessGas.gasLimits.daGas).toEqual(withKernelsGas.gasLimits.daGas); - expect(kernellessGas.gasLimits.l2Gas).toEqual(withKernelsGas.gasLimits.l2Gas); + expect(kernellessGas.totalGas.daGas).toEqual(withKernelsGas.totalGas.daGas); + expect(kernellessGas.totalGas.l2Gas).toEqual(withKernelsGas.totalGas.l2Gas); }); }); @@ -446,19 +444,17 @@ describe('Kernelless simulation', () => { wallet.setSimulationMode('kernelless-override'); const kernellessResult = await deployMethod.simulate(deployOptions); - const kernellessGas = kernellessResult.estimatedGas!; + const kernellessGas = kernellessResult.gasUsed!; wallet.setSimulationMode('full'); const withKernelsResult = await deployMethod.simulate(deployOptions); - const withKernelsGas = withKernelsResult.estimatedGas!; + const withKernelsGas = withKernelsResult.gasUsed!; - logger.info(`Schnorr kernelless gas: L2=${kernellessGas.gasLimits.l2Gas} DA=${kernellessGas.gasLimits.daGas}`); - logger.info( - `Schnorr with kernels gas: L2=${withKernelsGas.gasLimits.l2Gas} DA=${withKernelsGas.gasLimits.daGas}`, - ); + logger.info(`Schnorr kernelless gas: L2=${kernellessGas.totalGas.l2Gas} DA=${kernellessGas.totalGas.daGas}`); + logger.info(`Schnorr with kernels gas: L2=${withKernelsGas.totalGas.l2Gas} DA=${withKernelsGas.totalGas.daGas}`); - expect(kernellessGas.gasLimits.daGas).toEqual(withKernelsGas.gasLimits.daGas); - expect(kernellessGas.gasLimits.l2Gas).toEqual(withKernelsGas.gasLimits.l2Gas); + expect(kernellessGas.totalGas.daGas).toEqual(withKernelsGas.totalGas.daGas); + expect(kernellessGas.totalGas.l2Gas).toEqual(withKernelsGas.totalGas.l2Gas); }); it('simulates ECDSA account deployment and gas matches with-kernels counterpart', async () => { @@ -474,17 +470,17 @@ describe('Kernelless simulation', () => { wallet.setSimulationMode('kernelless-override'); const kernellessResult = await deployMethod.simulate(deployOptions); - const kernellessGas = kernellessResult.estimatedGas!; + const kernellessGas = kernellessResult.gasUsed!; wallet.setSimulationMode('full'); const withKernelsResult = await deployMethod.simulate(deployOptions); - const withKernelsGas = withKernelsResult.estimatedGas!; + const withKernelsGas = withKernelsResult.gasUsed!; - logger.info(`ECDSA kernelless gas: L2=${kernellessGas.gasLimits.l2Gas} DA=${kernellessGas.gasLimits.daGas}`); - logger.info(`ECDSA with kernels gas: L2=${withKernelsGas.gasLimits.l2Gas} DA=${withKernelsGas.gasLimits.daGas}`); + logger.info(`ECDSA kernelless gas: L2=${kernellessGas.totalGas.l2Gas} DA=${kernellessGas.totalGas.daGas}`); + logger.info(`ECDSA with kernels gas: L2=${withKernelsGas.totalGas.l2Gas} DA=${withKernelsGas.totalGas.daGas}`); - expect(kernellessGas.gasLimits.daGas).toEqual(withKernelsGas.gasLimits.daGas); - expect(kernellessGas.gasLimits.l2Gas).toEqual(withKernelsGas.gasLimits.l2Gas); + expect(kernellessGas.totalGas.daGas).toEqual(withKernelsGas.totalGas.daGas); + expect(kernellessGas.totalGas.l2Gas).toEqual(withKernelsGas.totalGas.l2Gas); }); }); }); 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 b8ccfb7e288a..863e08e69bbc 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 @@ -12,7 +12,13 @@ import { getBlobsPerL1Block, getPrefixedEthBlobCommitments, } from '@aztec/blob-lib'; -import { GENESIS_ARCHIVE_ROOT, MAX_NULLIFIERS_PER_TX, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; +import { + GENESIS_ARCHIVE_ROOT, + MAX_NULLIFIERS_PER_TX, + MAX_PROCESSABLE_L2_GAS, + MAX_TX_DA_GAS, + NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, +} from '@aztec/constants'; import { EpochCache } from '@aztec/epoch-cache'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { createExtendedL1Client } from '@aztec/ethereum/client'; @@ -65,7 +71,7 @@ import { getNextL1SlotTimestamp, getSlotStartBuildTimestamp, } from '@aztec/stdlib/epoch-helpers'; -import { GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { tryStop } from '@aztec/stdlib/interfaces/server'; import { CheckpointProposal, @@ -397,7 +403,10 @@ describe('L1Publisher integration', () => { chainId: fr(chainId), version: fr(version), vkTreeRoot: getVKTreeRoot(), - gasSettings: GasSettings.fallback({ maxFeesPerGas: minFee }), + gasSettings: GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS), + maxFeesPerGas: minFee, + }), protocolContracts: ProtocolContractsList, seed, }); 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 727bd706883a..3401cc5b5603 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 @@ -114,7 +114,7 @@ describe('e2e_multi_eoa', () => { // We intercept the transaction and delete it from Anvil. // We also do the same for any cancel transactions. // We should then see that another block is published but this time with a different expected account - const testAccountRotation = async (expectedFirstSender: number, expectedSecondSender: number) => { + const testAccountRotation = async () => { // the L2 tx we are going to try and execute const deployMethod = StatefulTestContract.deploy(wallet, defaultAccountAddress, 0, { salt: Fr.random(), @@ -124,14 +124,11 @@ describe('e2e_multi_eoa', () => { const l1Utils: L1TxUtils[] = (publisherManager as any).publishers; - const blockedSender = l1Utils[expectedFirstSender].getSenderAddress(); + let blockedSender: EthAddress | undefined; const blockedTxs: Hex[] = []; - const fallbackSender = l1Utils[expectedSecondSender].getSenderAddress(); - const fallbackTxs: Hex[] = []; + const fallbackTxs: { sender: EthAddress; txHash: Hex }[] = []; - logger.warn( - `Testing account rotation with blocked sender ${blockedSender} and fallback sender ${fallbackSender}`, - ); + logger.warn('Testing account rotation by blocking the first publisher attempted'); // Get unique clients - they may or may not be the same object const uniqueClients = [...new Set(l1Utils.map(u => u.client))]; @@ -144,6 +141,7 @@ describe('e2e_multi_eoa', () => { }), ); + blockedSender ??= signerAddress; if (blockedSender.equals(signerAddress)) { const txHash = randomEthTxHash(); // block this sender/ Its txs don't actually reach any L1 nodes blockedTxs.push(txHash); @@ -152,12 +150,8 @@ describe('e2e_multi_eoa', () => { } else { const originalFn = originalSendRawTransactions.get(this)!; const txHash = await originalFn.call(this, arg); - if (fallbackSender.equals(signerAddress)) { - logger.warn(`Found fallback tx from signer ${signerAddress.toString()} with hash ${txHash}`); - fallbackTxs.push(txHash); - } else { - logger.warn(`Found fallback tx from unexpected sender ${signerAddress.toString()} with hash ${txHash}`); - } + logger.warn(`Found fallback tx from signer ${signerAddress.toString()} with hash ${txHash}`); + fallbackTxs.push({ sender: signerAddress, txHash }); return txHash; } }; @@ -173,76 +167,33 @@ describe('e2e_multi_eoa', () => { const receipt = await waitForTx(aztecNode, txHash); expect(receipt.isMined() && receipt.hasExecutionSucceeded()).toBe(true); + expect(blockedSender).toBeDefined(); logger.warn(`Got ${blockedTxs.length} blocked txs for ${blockedSender}`); expect(blockedTxs.length).toBeGreaterThan(0); - logger.warn(`Got ${fallbackTxs.length} fallback txs for ${fallbackSender}`); + logger.warn(`Got ${fallbackTxs.length} fallback txs`); expect(fallbackTxs.length).toBeGreaterThan(0); - const transactionHashToKeep = fallbackTxs.at(-1)!; + const fallbackTx = fallbackTxs.at(-1)!; + expect(fallbackTx.sender.equals(blockedSender!)).toBeFalse(); const l1Tx = await ethCheatCodes.publicClient.getTransaction({ - hash: transactionHashToKeep, + hash: fallbackTx.txHash, }); const senderEthAddress = EthAddress.fromString(l1Tx.from); - const expectedSenderEthAddress = EthAddress.fromString(sequencerKeysAndAddresses[expectedSecondSender].address); - const areSame = senderEthAddress.equals(expectedSenderEthAddress); - expect(areSame).toBeTrue(); + expect(senderEthAddress.equals(fallbackTx.sender)).toBeTrue(); // Dispose of all spies spies.forEach(spy => spy.mockRestore()); }; it('publishers are rotated by the sequencer', async () => { - // Helpers to identify which accounts are expected to be used - const getSortedAddressesByBalance = async (addressAndKeys: { address: `0x${string}` }[]) => { - const addressesWithBalance = await Promise.all( - addressAndKeys.map(async ka => { - return { - balance: await ethCheatCodes.publicClient.getBalance({ address: ka.address }), - address: ka.address, - }; - }), - ); - - const sortedAddresses = addressesWithBalance.sort((a, b) => Number(b.balance - a.balance)); - return sortedAddresses; - }; - - const getAddressIndex = (address: `0x${string}`) => { - return sequencerKeysAndAddresses.findIndex(ka => ka.address === address); - }; - // 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). const blockNumber = await aztecNode.getBlockNumber(); expect(blockNumber).toBeGreaterThanOrEqual(2); - // This means that 2 of our accounts have been used to send blocks to L1. - // We want to figure out which ones these are, they will be in the 'MINED' state within the sequencer - const sortedAddresses = await getSortedAddressesByBalance(sequencerKeysAndAddresses); - - // We expect the highest balance account to be used first, then the second highest balance account - await testAccountRotation( - getAddressIndex(sortedAddresses[0].address), - getAddressIndex(sortedAddresses[1].address), - ); - - // The first sender used above will now be out of action as it is unable to get anything MINED. - const validAddresses = sortedAddresses.slice(1); - logger.warn(`Removing invalidated publisher ${sortedAddresses[0].address}`, { - validAddresses, - invalidAddress: sortedAddresses[0], - }); - - const sortedValidAddresses = await getSortedAddressesByBalance(validAddresses); - logger.warn(`Re-sorted valid addresses by balance`, { sortedValidAddresses }); - - // All of our valid addresses have published transactions so will be in MINED state - // the sequencer should select the 2 highest balance accounts in this next test - await testAccountRotation( - getAddressIndex(sortedValidAddresses[0].address), - getAddressIndex(sortedValidAddresses[1].address), - ); + await testAccountRotation(); + await testAccountRotation(); }); }); }); 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 f14dacab2420..a68ea4b91c57 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 @@ -10,6 +10,7 @@ import { FIELDS_PER_BLOB } from '@aztec/constants'; import { AvmGadgetsTestContract } from '@aztec/noir-test-contracts.js/AvmGadgetsTest'; import { AvmTestContract } from '@aztec/noir-test-contracts.js/AvmTest'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; +import { type Sequencer, type SequencerClient, type SequencerEvents, SequencerState } from '@aztec/sequencer-client'; import { L2Block } from '@aztec/stdlib/block'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; @@ -23,20 +24,47 @@ describe('e2e_multiple_blobs', () => { let defaultAccountAddress: AztecAddress; let aztecNode: AztecNode; let aztecNodeAdmin: AztecNodeAdmin; + let sequencer: Sequencer; let teardown: () => Promise; + function waitForSequencerIdle(timeout = 30000): Promise { + if (sequencer.status().state === SequencerState.IDLE) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + sequencer.off('state-changed', handler); + reject(new Error('Timeout waiting for sequencer IDLE state')); + }, timeout); + + const handler = (args: Parameters[0]) => { + if (args.newState === SequencerState.IDLE) { + clearTimeout(timer); + sequencer.off('state-changed', handler); + resolve(); + } + }; + + sequencer.on('state-changed', handler); + }); + } + beforeAll(async () => { let maybeAztecNodeAdmin: AztecNodeAdmin | undefined; + let maybeSequencer: SequencerClient | undefined; ({ logger, wallet, accounts: [defaultAccountAddress], aztecNode, aztecNodeAdmin: maybeAztecNodeAdmin, + sequencer: maybeSequencer, wallet, teardown, } = await setup(1, { ...PIPELINING_SETUP_OPTS })); aztecNodeAdmin = maybeAztecNodeAdmin!; + sequencer = maybeSequencer!.getSequencer(); ({ contract } = await TestContract.deploy(wallet).send({ from: defaultAccountAddress })); }); @@ -47,6 +75,7 @@ describe('e2e_multiple_blobs', () => { // Increase the minimum number of txs per block so that all txs will be mined in the same block. const TX_COUNT = 3; await aztecNodeAdmin.setConfig({ minTxsPerBlock: TX_COUNT }); + await waitForSequencerIdle(); const provenTxs = [ // 2 contract deployment txs. 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 3ed31f0279e9..490532e4fd6d 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 @@ -97,8 +97,6 @@ describe('e2e_p2p_add_rollup', () => { await t.removeInitialNode(); l1TxUtils = createL1TxUtils(t.ctx.deployL1ContractsValues.l1Client); - - t.ctx.watcher.setIsMarkingAsProven(false); }); afterAll(async () => { 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 b2c43d292771..daf0d16c481e 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 @@ -26,6 +26,7 @@ const BOOT_NODE_UDP_PORT = 4500; const COMMITTEE_SIZE = NUM_VALIDATORS; const ETHEREUM_SLOT_DURATION = 4; const AZTEC_SLOT_DURATION = ETHEREUM_SLOT_DURATION * 2; +const BLOCK_DURATION_MS = 2000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'broadcasted-invalid-block-proposal-slash-')); @@ -62,6 +63,7 @@ describe('e2e_p2p_broadcasted_invalid_block_proposal_slash', () => { aztecEpochDuration, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecSlotDuration: AZTEC_SLOT_DURATION, + blockDurationMs: BLOCK_DURATION_MS, aztecTargetCommitteeSize: COMMITTEE_SIZE, inboxLag: 2, aztecProofSubmissionEpochs: 1024, // effectively do not reorg 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 b63188b4687e..5e394b33192e 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 @@ -83,7 +83,6 @@ describe('e2e_p2p_duplicate_attestation_slash', () => { slashAmountSmall: slashingUnit, slashAmountMedium: slashingUnit * 2n, slashAmountLarge: slashingUnit * 3n, - enforceTimeTable: true, blockDurationMs: BLOCK_DURATION * 1000, slashDuplicateProposalPenalty: slashingUnit, slashDuplicateAttestationPenalty: slashingUnit, 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 2fdc4f02074a..07f778961cd7 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 @@ -76,7 +76,6 @@ describe('e2e_p2p_duplicate_proposal_slash', () => { slashAmountSmall: slashingUnit, slashAmountMedium: slashingUnit * 2n, slashAmountLarge: slashingUnit * 3n, - enforceTimeTable: true, blockDurationMs: BLOCK_DURATION * 1000, slashDuplicateProposalPenalty: slashingUnit, slashingOffsetInRounds: 1, 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 399d169c6497..305914f58b31 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 @@ -33,6 +33,7 @@ const NUM_TXS_PER_NODE = 2; const BOOT_NODE_UDP_PORT = process.env.BOOT_NODE_UDP_PORT ? parseInt(process.env.BOOT_NODE_UDP_PORT) : 4500; const AZTEC_SLOT_DURATION = 36; const AZTEC_EPOCH_DURATION = 4; +const BLOCK_DURATION_MS = 16_000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'gossip-')); @@ -66,6 +67,7 @@ describe('e2e_p2p_network', () => { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecSlotDuration: AZTEC_SLOT_DURATION, aztecEpochDuration: AZTEC_EPOCH_DURATION, + blockDurationMs: BLOCK_DURATION_MS, slashingRoundSizeInEpochs: 2, slashingQuorum: 5, listenAddress: '127.0.0.1', 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 44ec7e22f02f..a9837a69efee 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 @@ -34,6 +34,7 @@ const CHECK_ALERTS = process.env.CHECK_ALERTS === 'true'; const NUM_VALIDATORS = 4; const NUM_TXS_PER_NODE = 2; const BOOT_NODE_UDP_PORT = 4500; +const BLOCK_DURATION_MS = 10_000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'gossip-')); @@ -63,6 +64,7 @@ describe('e2e_p2p_network', () => { initialConfig: { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecSlotDuration: 24, + blockDurationMs: BLOCK_DURATION_MS, listenAddress: '127.0.0.1', // Allow empty blocks so the first checkpoint can be published before any txs are submitted. // Without this, no blocks are built until txs arrive, and a failed checkpoint during tx 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 72e9b393d27f..146828dbf375 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 @@ -49,7 +49,6 @@ describe('e2e_p2p_multiple_validators_sentinel', () => { aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, blockDurationMs: 6000, - enforceTimeTable: true, aztecProofSubmissionEpochs: 1024, // effectively do not reorg listenAddress: '127.0.0.1', minTxsPerBlock: 0, 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 2ad368fd5262..8061bce031fe 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 @@ -5,6 +5,7 @@ import { TxHash } from '@aztec/aztec.js/tx'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Signature } from '@aztec/foundation/eth-signature'; import { retryUntil } from '@aztec/foundation/retry'; +import { sleep } from '@aztec/foundation/sleep'; import { ENR, type P2PClient, type P2PService, type PeerId } from '@aztec/p2p'; import type { SequencerClient } from '@aztec/sequencer-client'; import { CheckpointAttestation, ConsensusPayload } from '@aztec/stdlib/p2p'; @@ -40,6 +41,7 @@ const NUM_VALIDATORS = 3; const NUM_PREFERRED_NODES = 2; const NUM_TXS_PER_NODE = 2; const BOOT_NODE_UDP_PORT = 4500; +const BLOCK_DURATION_MS = 10_000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'gossip-')); @@ -138,6 +140,7 @@ describe('e2e_p2p_preferred_network', () => { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecSlotDuration: 24, aztecEpochDuration: 4, + blockDurationMs: BLOCK_DURATION_MS, listenAddress: '127.0.0.1', p2pDisableStatusHandshake: false, // Just for testing be aggressive here, don't allow any auth handshake failures @@ -336,6 +339,17 @@ describe('e2e_p2p_preferred_network', () => { // blocks without them (since targetCommitteeSize is set to the number of nodes) await t.setupAccount(); + t.logger.info('Waiting for first checkpoint to be published'); + await retryUntil( + async () => (await validators[0].getBlockNumber('checkpointed')) > 0, + 'first checkpoint published', + 120, + ); + t.logger.info('First checkpoint published'); + + const ethereumSlotDuration = t.ctx.aztecNodeConfig.ethereumSlotDuration ?? 4; + await sleep((ethereumSlotDuration + 1) * 1000); + // Send the required number of transactions to each node t.logger.info('Submitting transactions'); for (const node of nodes) { 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 2379d8bcc7ec..36aa8ecc3b15 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 @@ -16,6 +16,7 @@ import { submitTransactions } from './shared.js'; const NUM_VALIDATORS = 4; const NUM_TXS_PER_NODE = 2; const BOOT_NODE_UDP_PORT = 4500; +const BLOCK_DURATION_MS = 10_000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'rediscovery-')); @@ -34,6 +35,7 @@ describe('e2e_p2p_rediscovery', () => { initialConfig: { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecSlotDuration: 24, + blockDurationMs: BLOCK_DURATION_MS, listenAddress: '127.0.0.1', inboxLag: 2, }, @@ -100,8 +102,7 @@ describe('e2e_p2p_rediscovery', () => { } nodes = newNodes; - // wait a bit for peers to discover each other - await sleep(2000); + await t.waitForP2PMeshConnectivity(newNodes, NUM_VALIDATORS, 120); for (const node of newNodes) { const txs = await submitTransactions(t.logger, node, NUM_TXS_PER_NODE, t.fundedAccount); 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 42f238402bea..59527d18f17e 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 @@ -43,7 +43,6 @@ describe('e2e_p2p_reex', () => { // To collect metrics - run in aztec-packages `docker compose --profile metrics up` and set COLLECT_METRICS=true metricsPort: shouldCollectMetrics(), initialConfig: { - enforceTimeTable: true, txTimeoutMs: 30_000, listenAddress: '127.0.0.1', aztecProofSubmissionEpochs: 1024, // effectively do not reorg diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts index 9b1710df54a5..19e06a4fc14d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp/utils.ts @@ -44,7 +44,6 @@ export async function createReqrespTest(options: ReqrespOptions = {}): Promise

{ aztecTargetCommitteeSize: COMMITTEE_SIZE, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + blockDurationMs: BLOCK_DURATION_MS, aztecEpochDuration: AZTEC_EPOCH_DURATION, aztecProofSubmissionEpochs: 1024, minTxsPerBlock: 0, 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 9713acdf8964..dc7fa791e0a4 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 @@ -30,6 +30,7 @@ const NUM_VALIDATORS = NUM_NODES + 1; // We create an extra validator, who will const BOOT_NODE_UDP_PORT = 4500; const ETHEREUM_SLOT_DURATION = 4; const AZTEC_SLOT_DURATION = 8; +const BLOCK_DURATION_MS = 2000; const EPOCH_DURATION = 2; // how many l2 slots make up a slashing round const SLASHING_ROUND_SIZE = 4; @@ -76,6 +77,7 @@ describe('veto slash', () => { anvilSlotsInAnEpoch: 4, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + blockDurationMs: BLOCK_DURATION_MS, aztecProofSubmissionEpochs: 1024, // effectively do not reorg listenAddress: '127.0.0.1', minTxsPerBlock: 0, 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 c05f9b9dc1d6..7bb7003a8ee0 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 @@ -20,6 +20,7 @@ const BLOCK_COUNT = 3; const EPOCH_DURATION = 2; const ETHEREUM_SLOT_DURATION = 4; const AZTEC_SLOT_DURATION = 8; +const BLOCK_DURATION_MS = 2000; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'validators-sentinel-')); @@ -43,6 +44,7 @@ describe('e2e_p2p_validators_sentinel', () => { aztecTargetCommitteeSize: NUM_VALIDATORS, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, + blockDurationMs: BLOCK_DURATION_MS, aztecProofSubmissionEpochs: 1024, // effectively do not reorg listenAddress: '127.0.0.1', minTxsPerBlock: 0, 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 460ddd1b4a26..c241727c9aeb 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 @@ -94,8 +94,8 @@ describe('e2e_pruned_blocks', () => { ).toBeGreaterThan(0); // Mine enough empty blocks past the first mint block so it becomes eligible for pruning, then - // mark the chain as proven. AUTOMINE_E2E_OPTS disables AnvilTestWatcher (no auto-markAsProven - // loop) and no EpochTestSettler is wired in the e2e fixture, so we mark explicitly here. + // mark the chain as proven. Under AUTOMINE_E2E_OPTS the AutomineSequencer does not mark blocks + // proven and no EpochTestSettler is wired in the e2e fixture, so we mark explicitly here. // World-state prunes on the chain-finalized event; with Anvil's `finalized = latest - 2` // heuristic, we need a couple of additional L1 blocks after markAsProven so the archiver's // `getFinalizedL1Block` query resolves to a block that already sees the new proven tip. 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 7349ad24b24c..f08cdced0834 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 @@ -72,10 +72,10 @@ describe('e2e_escape_hatch_vote_only', () => { // Override PIPELINING_SETUP_OPTS slot durations for the longer cadence this test needs. ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecSlotDuration: AZTEC_SLOT_DURATION, + blockDurationMs: 12000, aztecEpochDuration: AZTEC_EPOCH_DURATION, // Keep pruning far away for this test. aztecProofSubmissionEpochs: 15, // needed so ACTIVE_DURATION=2 is a valid EscapeHatch config - enforceTimeTable: true, automineL1Setup: true, // Pipelining opts — exercise the §6 B5 fix (tryVoteWhenEscapeHatchOpen signing/submitting for targetSlot). // inboxLag: 2 so the sequencer sources L1->L2 messages from a sealed checkpoint when building for slot+1. 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 c407f8658e3e..ba0b9048a3c8 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 @@ -78,7 +78,6 @@ describe('e2e_gov_proposal', () => { aztecSlotDuration: AZTEC_SLOT_DURATION, aztecProofSubmissionEpochs: 128, // no pruning minTxsPerBlock: TXS_PER_BLOCK, - enforceTimeTable: true, automineL1Setup: true, // speed up setup // Force the L1 sync to fetch blobs rather than promote the locally-proposed checkpoint. // The "should vote even when unable to build blocks" test relies on the blob client being the 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 89c975f297b6..601785e53a36 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 @@ -4,11 +4,12 @@ import { ContractDeployer } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import { type AztecNode, waitForTx } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; +import type { CheatCodes } from '@aztec/aztec/testing'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { SecretValue } from '@aztec/foundation/config'; import type { EthPrivateKey } from '@aztec/node-keystore'; import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest'; -import type { SequencerClient } from '@aztec/sequencer-client'; +import { type SequencerClient, type SequencerEvents, SequencerState } from '@aztec/sequencer-client'; import type { TestSequencer, TestSequencerClient } from '@aztec/sequencer-client/test'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import type { ValidatorClient } from '@aztec/validator-client'; @@ -32,12 +33,64 @@ const VALIDATOR_COUNT = 4; const COMMITTEE_SIZE = VALIDATOR_COUNT; const INITIAL_KEYSTORE_COUNT = 3; +async function waitForSequencerIdleAfter( + sequencer: TestSequencer, + action: () => Promise, + timeout = 30_000, +): Promise { + let settled = false; + let resolveIdle!: () => void; + let rejectIdle!: (err: Error) => void; + const idlePromise = new Promise((resolve, reject) => { + resolveIdle = resolve; + rejectIdle = reject; + }); + + let cleanup = () => {}; + const finish = (complete: () => void) => { + if (settled) { + return; + } + settled = true; + cleanup(); + complete(); + }; + + const handler = (args: Parameters[0]) => { + if (args.newState === SequencerState.IDLE) { + finish(resolveIdle); + } + }; + + const timer = setTimeout( + () => finish(() => rejectIdle(new Error('Timeout waiting for sequencer IDLE state'))), + timeout, + ); + cleanup = () => { + clearTimeout(timer); + sequencer.off('state-changed', handler); + }; + + sequencer.on('state-changed', handler); + try { + await action(); + if (sequencer.status().state === SequencerState.IDLE) { + finish(resolveIdle); + } + await idlePromise; + } catch (err) { + finish(() => {}); + throw err; + } +} + describe('e2e_reload_keystore', () => { jest.setTimeout(540_000); let teardown: () => Promise; let aztecNode: AztecNode; let aztecNodeAdmin: AztecNodeAdmin | undefined; + let cheatCodes: CheatCodes; let wallet: Wallet; let ownerAddress: AztecAddress; let keyStoreDirectory: string; @@ -89,6 +142,7 @@ describe('e2e_reload_keystore', () => { teardown, aztecNode, aztecNodeAdmin, + cheatCodes, wallet, accounts: [ownerAddress], sequencer: sequencerClient, @@ -113,8 +167,8 @@ describe('e2e_reload_keystore', () => { 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(); - const validatorClient: ValidatorClient = (sequencer as TestSequencer).validatorClient; + const sequencer = (sequencerClient! as TestSequencerClient).getSequencer() as TestSequencer; + const validatorClient: ValidatorClient = sequencer.validatorClient; // Verify initial keystore state and block production // Only the first 3 validators should be loaded @@ -181,7 +235,7 @@ describe('e2e_reload_keystore', () => { // Directly ask the publisher factory to create a publisher for validator 4. // This exercises the full chain: keystore lookup → publisher filter → L1 signer match. // If the publisher key weren't in the L1TxUtils pool, this would throw. - const publisherFactory = (sequencer as TestSequencer).publisherFactory; + const publisherFactory = sequencer.publisherFactory; const validator4Attestor = EthAddress.fromString(validatorAddresses[3]); const { attestorAddress: returnedAttestor, publisher: validator4Publisher } = await publisherFactory.create(validator4Attestor); @@ -193,6 +247,9 @@ describe('e2e_reload_keystore', () => { // Verify block production uses new coinbases (not old) // Send a tx and confirm the block uses one of the new per-validator coinbases. // Whichever validator is the proposer, its coinbase must be from the reloaded keystore. + // A checkpoint job may already be waiting for txs with the old coinbase, so let it expire before sending. + await waitForSequencerIdleAfter(sequencer, () => cheatCodes.rollup.advanceToNextSlot()); + const allNewCoinbasesLower = newCoinbases.map(c => c.toString().toLowerCase()); const { txHash: sentTx2 } = await deployer 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 bf91c467acab..4ab9a7038671 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 @@ -5,6 +5,7 @@ import type { TxReceipt } from '@aztec/aztec.js/tx'; import { Bot, type BotConfig, BotStore, getBotDefaultConfig } from '@aztec/bot'; import { MAX_TX_DA_GAS } from '@aztec/constants'; import type { Logger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import type { SequencerClient } from '@aztec/sequencer-client'; import { EmbeddedWallet } from '@aztec/wallets/embedded'; @@ -101,11 +102,19 @@ describe('e2e_sequencer_config', () => { expect(receipt2).toBeDefined(); expect(receipt2.hasExecutionSucceeded()).toBe(true); + const checkpointedBeforeLimitReduction = await aztecNode.getBlockNumber('checkpointed'); + // Set the maxL2BlockGas to the total mana used - 1 sequencer!.updateConfig({ maxL2BlockGas: Number(totalManaUsed) - 1, }); + await retryUntil( + async () => (await aztecNode.getBlockNumber('checkpointed')) > checkpointedBeforeLimitReduction, + 'checkpoint after lowering maxL2BlockGas', + PIPELINING_SETUP_OPTS.aztecSlotDuration * 4, + ); + // Try to run a tx and expect it to fail await expect(bot.run()).rejects.toThrow(/Timeout awaiting isMined/); }); 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 62b6abd98cc1..3b141fa5044c 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 @@ -182,7 +182,6 @@ describe('e2e_slashing_attested_invalid_proposal', () => { minBlocksForCheckpoint: BLOCKS_PER_CHECKPOINT, maxBlocksPerCheckpoint: BLOCKS_PER_CHECKPOINT, publishTxsWithProposals: true, - enforceTimeTable: true, blockDurationMs: BLOCK_DURATION_MS, attestationPropagationTime: 0.5, slashDuplicateProposalPenalty: 1n, 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 85af7c548c3e..c6e9800bf187 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 @@ -37,6 +37,7 @@ const COMMITTEE_SIZE = NUM_VALIDATORS; const ETHEREUM_SLOT_DURATION = 4; const AZTEC_EPOCH_DURATION = 2; const AZTEC_SLOT_DURATION = ETHEREUM_SLOT_DURATION * AZTEC_EPOCH_DURATION; +const BLOCK_DURATION_MS = 2000; const SLASHING_QUORUM = 5; const SLASHING_ROUND_SIZE = 8; const TERMINAL_BLOCK_INDEX = IndexWithinCheckpoint(1); @@ -243,6 +244,7 @@ describe('e2e_slashing_broadcasted_invalid_checkpoint_proposal_slash', () => { aztecEpochDuration: AZTEC_EPOCH_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecSlotDuration: AZTEC_SLOT_DURATION, + blockDurationMs: BLOCK_DURATION_MS, aztecTargetCommitteeSize: COMMITTEE_SIZE, aztecProofSubmissionEpochs: 1024, minTxsPerBlock: 0, 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 ed4cfb10adbe..96047cc18ead 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -39,7 +39,6 @@ import { BatchCall, type Contract, NO_WAIT } from '@aztec/aztec.js/contracts'; import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; import { waitForTx } from '@aztec/aztec.js/node'; -import { AnvilTestWatcher } from '@aztec/aztec/testing'; import { createBlobClientWithFileStores } from '@aztec/blob-client/client'; import { Blob } from '@aztec/blob-lib'; import { EpochCache } from '@aztec/epoch-cache'; @@ -419,7 +418,6 @@ describe('e2e_synching', () => { cheatCodes, aztecNode, sequencer, - watcher, wallet, initialFundedAccounts, dateProvider, @@ -431,7 +429,6 @@ describe('e2e_synching', () => { await (aztecNode as any).stop(); await (sequencer as any).stop(); - await watcher.stop(); const blobClient = await createBlobClientWithFileStores(config, createLogger('test:blob-client:client')); @@ -557,13 +554,6 @@ describe('e2e_synching', () => { const contracts: Contract[] = []; { - const watcher = new AnvilTestWatcher( - opts.cheatCodes!.eth, - opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress, - opts.deployL1ContractsValues!.l1Client, - ); - await watcher.start(); - const aztecNode = await AztecNodeService.createAndSync(opts.config!, {}, { genesis: opts.genesis }); const sequencer = aztecNode.getSequencer(); @@ -589,7 +579,6 @@ describe('e2e_synching', () => { ).contract, ); - await watcher.stop(); await sequencer?.stop(); await aztecNode.stop(); } @@ -701,13 +690,6 @@ describe('e2e_synching', () => { await opts.cheatCodes!.eth.warp(Number(timeJumpTo), { resetBlockInterval: true }); - const watcher = new AnvilTestWatcher( - opts.cheatCodes!.eth, - opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress, - opts.deployL1ContractsValues!.l1Client, - ); - await watcher.start(); - await opts.deployL1ContractsValues!.l1Client.waitForTransactionReceipt({ hash: await rollup.write.prune(), }); @@ -732,7 +714,6 @@ describe('e2e_synching', () => { await sequencer?.stop(); await aztecNode.stop(); - await watcher.stop(); }, ASSUME_PROVEN_THROUGH, ); @@ -764,13 +745,6 @@ describe('e2e_synching', () => { await rollup.write.prune(); - const watcher = new AnvilTestWatcher( - opts.cheatCodes!.eth, - opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress, - opts.deployL1ContractsValues!.l1Client, - ); - await watcher.start(); - // The sync here could likely be avoided by using the node we just synched. const aztecNode = await AztecNodeService.createAndSync(opts.config!, {}, { genesis: opts.genesis }); const sequencer = aztecNode.getSequencer(); @@ -791,7 +765,6 @@ describe('e2e_synching', () => { await sequencer?.stop(); await aztecNode.stop(); - await watcher.stop(); }, ASSUME_PROVEN_THROUGH, ); diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 906aa5b9ec20..05a6cb46064f 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -141,9 +141,6 @@ export class FullProverTest { this.logger.info(`Enabling proving`, { realProofs: this.realProofs }); - // We don't wish to mark as proven automatically, so we set the flag to false - this.context.watcher.setIsMarkingAsProven(false); - this.simulatedProverAztecNode = this.context.proverNode!; ({ aztecNode: this.aztecNode, diff --git a/yarn-project/end-to-end/src/fixtures/fixtures.ts b/yarn-project/end-to-end/src/fixtures/fixtures.ts index f03908882227..45979253b366 100644 --- a/yarn-project/end-to-end/src/fixtures/fixtures.ts +++ b/yarn-project/end-to-end/src/fixtures/fixtures.ts @@ -26,13 +26,19 @@ export const PIPELINED_FEE_PADDING = 30; * * await setup(N, { ...PIPELINING_SETUP_OPTS, ...otherOpts }); * - * The preset sets: + * The preset runs the production Sequencer with the always-enforced timetable at real (wall-clock) + * timing, yielding exactly 2 blocks per slot. It sets: * - `inboxLag: 2` so the sequencer sources L1->L2 messages from checkpoint N-1 (already sealed), * avoiding `L1ToL2MessagesNotReadyError` when building for slot N during slot N-1. * - `minTxsPerBlock: 0` so empty checkpoints land even when a tx arrives late in the build window * (otherwise the chain stalls on alternating slots). * - `aztecSlotDuration: 12` / `ethereumSlotDuration: 4` so the pipelined cycle fits inside the * default 300s Jest hook budget. Tests that depend on the env-default 72s/12s should override. + * - `blockDurationMs: 3000` to cut exactly 2 blocks per slot. With `ethereumSlotDuration < 8` the + * timing model normalizes to `init=0.5`, `assemble=0.5`, `P=0`, `minExec=1`, so + * `maxBlocks = floor((S - init - (assemble + 2P + D)) / D) = floor((12 - 0.5 - (0.5 + 0 + 3)) / 3) + * = floor(8/3) = 2`. (`blockDurationMs: 2000` would give 4 blocks/slot; 3000 also matches the + * production default.) * - `walletMinFeePadding: PIPELINED_FEE_PADDING` (30x) to absorb the wider fee evolution window. */ export const PIPELINING_SETUP_OPTS = { @@ -40,6 +46,7 @@ export const PIPELINING_SETUP_OPTS = { minTxsPerBlock: 0, aztecSlotDuration: 12, ethereumSlotDuration: 4, + blockDurationMs: 3000, walletMinFeePadding: PIPELINED_FEE_PADDING, } as const; @@ -55,10 +62,7 @@ export const PIPELINING_SETUP_OPTS = { * - Swaps the production Sequencer for an AutomineSequencer that builds one block per * submitted tx, publishes synchronously to L1, and owns all time control through a * serial queue (see `sequencer-client/src/sequencer/automine/automine_sequencer.ts`). - * - Disables the validator client and AnvilTestWatcher (the AutomineSequencer needs - * neither). - * - Disables orphan proposed-block pruning in the archiver because automine owns test - * time and can advance past prune deadlines before local tx receipt polling observes the mined block. + * - Disables the validator client (the AutomineSequencer needs none). * - Uses `inboxLag: 1` (synchronous) since the AutomineSequencer publishes one block per tx. * - Switches anvil into automine mode at setup time (no interval mining); each L1 tx * mines an L1 block immediately. @@ -67,8 +71,6 @@ export const PIPELINING_SETUP_OPTS = { */ export const AUTOMINE_E2E_OPTS = { useAutomineSequencer: true, - disableAnvilTestWatcher: true, - skipOrphanProposedBlockPruning: true, inboxLag: 1, minTxsPerBlock: 0, aztecSlotDuration: 12, diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 94b1934ed123..9f67afe9c33d 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -17,7 +17,7 @@ import { Fr } from '@aztec/aztec.js/fields'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; -import { AnvilTestWatcher, type AnvilTestWatcherOpts, CheatCodes } from '@aztec/aztec/testing'; +import { CheatCodes } from '@aztec/aztec/testing'; import { SPONSORED_FPC_SALT } from '@aztec/constants'; import { isAnvilTestChain } from '@aztec/ethereum/chain'; import { createExtendedL1Client } from '@aztec/ethereum/client'; @@ -187,9 +187,6 @@ export type SetupOptions = { mockGossipSubNetwork?: boolean; /** Whether to add simulated latency to the mock gossipsub network (in ms) */ mockGossipSubNetworkLatency?: number; - /** Whether to disable the anvil test watcher (can still be manually started) */ - disableAnvilTestWatcher?: boolean; - anvilTestWatcherOpts?: AnvilTestWatcherOpts; /** Whether to enable anvil automine during deployment of L1 contracts (consider defaulting this to true). */ automineL1Setup?: boolean; /** How many accounts to seed and unlock in anvil. */ @@ -255,8 +252,6 @@ export type EndToEndContext = { cheatCodes: CheatCodes; /** The cheat codes for L1 */ ethCheatCodes: EthCheatCodes; - /** The anvil test watcher. */ - watcher: AnvilTestWatcher; /** Allows tweaking current system time, used by the epoch cache only. */ dateProvider: TestDateProvider; /** Telemetry client */ @@ -331,8 +326,6 @@ export async function setup( config.maxPendingTxCount = opts.maxPendingTxCount ?? TEST_MAX_PENDING_TX_POOL_COUNT; // For tests we only want proving enabled if specifically requested config.realProofs = !!opts.realProofs; - // Only enforce the time table if requested - config.enforceTimeTable = !!opts.enforceTimeTable; // Enable the tx delayer for tests (default config has it disabled, so we force-enable it here) config.enableDelayer = true; config.listenAddress = '127.0.0.1'; @@ -487,8 +480,8 @@ export async function setup( } // In compose mode (no local anvil), sync dateProvider to L1 time since it may have drifted - // ahead of system time due to the local-network watcher warping time forward on each filled slot. - // When running with a local anvil, the dateProvider is kept in sync via the stdout listener. + // ahead of system time. When running with a local anvil, the dateProvider is kept in sync via + // the stdout listener. if (!anvil) { dateProvider.setTime((await ethCheatCodes.lastBlockTimestamp()) * 1000); } @@ -497,17 +490,6 @@ export async function setup( await ethCheatCodes.warp(opts.l2StartTime, { resetBlockInterval: true }); } - const watcher = new AnvilTestWatcher( - new EthCheatCodesWithState(config.l1RpcUrls, dateProvider), - deployL1ContractsValues.l1ContractAddresses.rollupAddress, - deployL1ContractsValues.l1Client, - dateProvider, - opts.anvilTestWatcherOpts, - ); - if (!opts.disableAnvilTestWatcher) { - await watcher.start(); - } - // Use metricsPort-based telemetry if provided, otherwise use the regular telemetry client const telemetryClient = opts.metricsPort ? await getEndToEndTestTelemetryClient(opts.metricsPort) @@ -545,6 +527,7 @@ export async function setup( if (originalMinTxsPerBlock === undefined) { throw new Error('minTxsPerBlock is undefined in e2e test setup'); } + const originalBuildCheckpointIfEmpty = config.buildCheckpointIfEmpty ?? false; // Whether we're deploying accounts (and therefore need reliable block inclusion past genesis) const shouldDeployAccounts = numberOfAccounts > 0 && !opts.skipAccountDeployment; @@ -557,6 +540,11 @@ export async function setup( // Math.max(minTxsPerBlock ?? 1, 1) and still requires minValidTxs: 1. const accountsDeployMinTxs = 0; config.minTxsPerBlock = shouldDeployAccounts ? accountsDeployMinTxs : needsEmptyBlock ? 0 : originalMinTxsPerBlock; + const shouldTemporarilyBuildEmptyCheckpoints = + needsEmptyBlock && !opts.skipInitialSequencer && config.useAutomineSequencer !== true; + if (shouldTemporarilyBuildEmptyCheckpoints) { + config.buildCheckpointIfEmpty = true; + } config.p2pEnabled = opts.mockGossipSubNetwork || config.p2pEnabled; config.p2pIp = opts.p2pIp ?? config.p2pIp ?? '127.0.0.1'; @@ -674,8 +662,15 @@ export async function setup( // If skipAccountDeployment is true, we don't deploy or wait - tests will handle account deployment later // Now we restore the original minTxsPerBlock setting if we changed it. - if (sequencerClient && config.minTxsPerBlock !== originalMinTxsPerBlock) { - sequencerClient.getSequencer().updateConfig({ minTxsPerBlock: originalMinTxsPerBlock }); + if (sequencerClient) { + const sequencer = sequencerClient.getSequencer(); + if (config.minTxsPerBlock !== originalMinTxsPerBlock) { + sequencer.updateConfig({ minTxsPerBlock: originalMinTxsPerBlock }); + } + if (shouldTemporarilyBuildEmptyCheckpoints) { + sequencer.updateConfig({ buildCheckpointIfEmpty: originalBuildCheckpointIfEmpty }); + config.buildCheckpointIfEmpty = originalBuildCheckpointIfEmpty; + } } if (initialFundedAccounts.length < numberOfAccounts) { @@ -698,7 +693,6 @@ export async function setup( await bbConfig.cleanup(); } - await tryStop(watcher, logger); await tryStop(anvil, logger); await tryRmDir(directoryToCleanup, logger); @@ -736,7 +730,6 @@ export async function setup( telemetryClient, wallet, accounts, - watcher, acvmConfig, bbConfig, directoryToCleanup, diff --git a/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts b/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts index 674ef390f4db..99aba1bd5a95 100644 --- a/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts +++ b/yarn-project/end-to-end/src/forward-compatibility/wallet_service.ts @@ -74,7 +74,7 @@ async function main() { const rpcOptions = { maxBodySizeBytes: '50mb' }; // Serve node RPC - const nodeRpcServer = createNamespacedSafeJsonRpcServer({ node: [node, AztecNodeApiSchema] }, rpcOptions); + const nodeRpcServer = createNamespacedSafeJsonRpcServer({ aztec: [node, AztecNodeApiSchema] }, rpcOptions); const nodeHttpServer = await startHttpRpcServer(nodeRpcServer, { port: NODE_PORT }); logger.info(`Node JSON-RPC server listening on port ${nodeHttpServer.port}`); diff --git a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts index 1969c871b7c1..a923ecf4a625 100644 --- a/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts +++ b/yarn-project/end-to-end/src/shared/uniswap_l1_l2.ts @@ -88,8 +88,6 @@ export const uniswapL1L2TestSuite = ( l1Client = deployL1ContractsValues.l1Client; - t.watcher.setIsMarkingAsProven(false); - if (Number(await l1Client.getBlockNumber()) < expectedForkBlockNumber) { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } 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 b62b3e68d83d..26f6eb711f9f 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 @@ -258,7 +258,7 @@ describe('block capacity benchmark', () => { // 3. Re-enable block building const enabledAt = new Date().toISOString(); logger.info(`[${label}] Re-enabling block building`); - await updateSequencersConfig(config, { minTxsPerBlock: 1, enforceTimeTable: true }); + await updateSequencersConfig(config, { minTxsPerBlock: 1 }); await retryUntil( async () => { const configs = await getSequencersConfig(config); 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 1e69c69f12a3..da682f5fb489 100644 --- a/yarn-project/end-to-end/src/spartan/mbps.test.ts +++ b/yarn-project/end-to-end/src/spartan/mbps.test.ts @@ -86,7 +86,6 @@ describe('multi-blocks-per-slot network test', () => { minTxsPerBlock: 1, maxTxsPerBlock: 1, blockDurationMs: BLOCK_DURATION_MS, - enforceTimeTable: true, attestationPropagationTime: 0.5, }); logger.info( @@ -95,7 +94,6 @@ describe('multi-blocks-per-slot network test', () => { minTxsPerBlock: sequencerConfig.minTxsPerBlock, maxTxsPerBlock: sequencerConfig.maxTxsPerBlock, blockDurationMs: sequencerConfig.blockDurationMs, - enforceTimeTable: sequencerConfig.enforceTimeTable, attestationPropagationTime: sequencerConfig.attestationPropagationTime, })), )}`, 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 d6fd24d6a626..20e187efe53a 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 @@ -15,10 +15,11 @@ import { RunningPromise } from '@aztec/foundation/promise'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { BenchmarkingContract } from '@aztec/noir-test-contracts.js/Benchmarking'; -import { type Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { deriveSigningKey } from '@aztec/stdlib/keys'; import { TopicType } from '@aztec/stdlib/p2p'; import { Tx, TxHash, TxStatus } from '@aztec/stdlib/tx'; +import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; import { jest } from '@jest/globals'; import { mkdir, writeFile } from 'fs/promises'; @@ -324,18 +325,20 @@ describe('sustained N TPS test', () => { { salt }, ); const deployMethod = await manager.getDeployMethod(); - // Explicit gas estimation: BaseWallet's fallback bakes - // APPROXIMATE_MAX_DA_GAS_PER_BLOCK=196_608 daGas into deploys, which exceeds + // Explicit gas estimation: BaseWallet's fallback bakes ~196_608 daGas into deploys, which exceeds // the proposer's per-block fair-share daGas (~94k at 10 blocks/checkpoint // with pipelining). Estimate first, send with the result. EmbeddedWallet // does this automatically; TestWallet (used here via WorkerWallet) does not. const deploySim = await deployMethod.simulate({ from: NO_FROM, - fee: { paymentMethod: sponsor, estimateGas: true }, + fee: { paymentMethod: sponsor }, + includeMetadata: true, }); + const { txsLimits } = await aztecNode.getNodeInfo(); + const deployGasLimits = getGasLimits(deploySim.gasUsed!, Gas.from(txsLimits.gas)); await deployMethod.send({ from: NO_FROM, - fee: { paymentMethod: sponsor, gasSettings: deploySim.estimatedGas }, + fee: { paymentMethod: sponsor, gasSettings: deployGasLimits }, wait: { timeout: 2400 }, }); return address; @@ -359,11 +362,14 @@ describe('sustained N TPS test', () => { const deploySim = await deployInteraction.simulate({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, + includeMetadata: true, }); - logger.info('Benchmark contract deploy estimated gas', { gasLimits: deploySim.estimatedGas?.gasLimits }); + const { txsLimits } = await aztecNode.getNodeInfo(); + const benchmarkDeployGasLimits = getGasLimits(deploySim.gasUsed!, Gas.from(txsLimits.gas)); + logger.info('Benchmark contract deploy estimated gas', { gasLimits: benchmarkDeployGasLimits.gasLimits }); ({ contract: benchmarkContract } = await deployInteraction.send({ from: accountAddresses[0], - fee: { paymentMethod: sponsor, gasSettings: deploySim.estimatedGas }, + fee: { paymentMethod: sponsor, gasSettings: benchmarkDeployGasLimits }, })); logger.info('Benchmark contract deployed', { address: benchmarkContract.address.toString() }); @@ -374,9 +380,10 @@ describe('sustained N TPS test', () => { // Gas estimate is sender-independent, so one pre-warmed value for all senders. const estimateSim = await benchmarkContract.methods.sha256_hash_1024(Array(1024).fill(42)).simulate({ from: accountAddresses[0], - fee: { paymentMethod: sponsor, estimateGas: true }, + fee: { paymentMethod: sponsor }, + includeMetadata: true, }); - benchmarkGasEstimate = estimateSim.estimatedGas; + benchmarkGasEstimate = getGasLimits(estimateSim.gasUsed!, Gas.from(txsLimits.gas)); logger.info('Benchmark tx estimated gas', { gasLimits: benchmarkGasEstimate?.gasLimits }); const currentMinFees = await refreshMinFees(); diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 76a30d61c029..7a54c5292214 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -14,7 +14,9 @@ import { createExtendedL1Client } from '@aztec/ethereum/client'; import type { Logger } from '@aztec/foundation/log'; import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; +import { Gas } from '@aztec/stdlib/gas'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; +import { getGasLimits } from '@aztec/wallet-sdk/base-wallet'; import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing'; import { getACVMConfig } from '../fixtures/get_acvm_config.js'; @@ -141,8 +143,9 @@ async function deployAccountWithDiagnostics( const deployMethod = await account.getDeployMethod(); let gasSettings: any; if (estimateGas) { - const sim = await deployMethod.simulate({ from: NO_FROM, fee: { paymentMethod } }); - gasSettings = sim.estimatedGas; + const sim = await deployMethod.simulate({ from: NO_FROM, fee: { paymentMethod }, includeMetadata: true }); + const { txsLimits } = await aztecNode.getNodeInfo(); + gasSettings = getGasLimits(sim.gasUsed!, Gas.from(txsLimits.gas)); logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`); } diff --git a/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts b/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts index c8c9b394d0b2..ad6239219822 100644 --- a/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts +++ b/yarn-project/end-to-end/src/test-wallet/worker_wallet_schema.ts @@ -1,4 +1,5 @@ import { ExecutionPayloadSchema, SendOptionsSchema, WalletSchema } from '@aztec/aztec.js/wallet'; +import type { ApiSchema } from '@aztec/foundation/schemas'; import { schemas } from '@aztec/foundation/schemas'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Tx } from '@aztec/stdlib/tx'; @@ -6,7 +7,7 @@ import { Tx } from '@aztec/stdlib/tx'; import { z } from 'zod'; /** Schema for the WorkerWallet API — extends WalletSchema with proveTx and registerAccount. */ -export const WorkerWalletSchema = { +export const WorkerWalletSchema: ApiSchema = { ...WalletSchema, proveTx: z.function({ input: z.tuple([ExecutionPayloadSchema, SendOptionsSchema]), output: Tx.schema }), registerAccount: z.function({ input: z.tuple([schemas.Fr, schemas.Fr]), output: AztecAddress.schema }), diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 083b9ae02870..8578700b64ce 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -78,6 +78,7 @@ export type EnvVar = | 'DEBUG_P2P_DISABLE_COLOCATION_PENALTY' | 'ENABLE_PROVER_NODE' | 'USE_AUTOMINE_SEQUENCER' + | 'AUTOMINE_ENABLE_PROVE_EPOCH' | 'ETHEREUM_HOSTS' | 'ETHEREUM_DEBUG_HOSTS' | 'ETHEREUM_ALLOW_NO_DEBUG_HOSTS' @@ -117,7 +118,6 @@ export type EnvVar = | 'P2P_BATCH_TX_REQUESTER_TX_BATCH_SIZE' | 'P2P_BATCH_TX_REQUESTER_BAD_PEER_THRESHOLD' | 'P2P_BLOCK_CHECK_INTERVAL_MS' - | 'P2P_SLOT_CHECK_INTERVAL_MS' | 'P2P_BLOCK_REQUEST_BATCH_SIZE' | 'P2P_BOOTSTRAP_NODE_ENR_VERSION_CHECK' | 'P2P_BOOTSTRAP_NODES_AS_FULL_PEERS' @@ -138,6 +138,7 @@ export type EnvVar = | 'P2P_L2_QUEUE_SIZE' | 'P2P_MAX_GOSSIP_CLOCK_DISPARITY_MS' | 'P2P_MAX_PEERS' + | 'P2P_PEER_BAN_DURATION_SECONDS' | 'P2P_PEER_CHECK_INTERVAL_MS' | 'P2P_PEER_FAILED_BAN_TIME_MS' | 'P2P_PEER_PENALTY_VALUES' @@ -225,6 +226,7 @@ export type EnvVar = | 'SEQ_MAX_DA_BLOCK_GAS' | 'SEQ_MAX_L2_BLOCK_GAS' | 'SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER' + | 'SEQ_PER_BLOCK_DA_ALLOCATION_MULTIPLIER' | 'SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET' | 'SEQ_PUBLISHER_PRIVATE_KEY' | 'SEQ_PUBLISHER_PRIVATE_KEYS' @@ -234,7 +236,7 @@ export type EnvVar = | 'PUBLISHER_FUNDING_THRESHOLD' | 'PUBLISHER_FUNDING_AMOUNT' | 'SEQ_POLLING_INTERVAL_MS' - | 'SEQ_ENFORCE_TIME_TABLE' + | 'SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT' | 'SEQ_ATTESTATION_PROPAGATION_TIME' | 'SEQ_CHECKPOINT_PROPOSAL_PREPARE_TIME' | 'CHECKPOINT_PROPOSAL_SYNC_GRACE_SECONDS' diff --git a/yarn-project/kv-store/src/sqlite-opfs/worker.ts b/yarn-project/kv-store/src/sqlite-opfs/worker.ts index 5ba4fe0115fc..81b0a068cf0b 100644 --- a/yarn-project/kv-store/src/sqlite-opfs/worker.ts +++ b/yarn-project/kv-store/src/sqlite-opfs/worker.ts @@ -29,8 +29,7 @@ let db: Database | undefined; let dbPath: string | undefined; async function ensurePool(directory: string): Promise { - sqlite3 ??= await sqlite3InitModule(); - const s = sqlite3; + const s = (sqlite3 ??= await sqlite3InitModule()); if (!pool) { pool = await s.installOpfsSAHPoolVfs({ name: SAH_POOL_VFS_NAME, @@ -68,8 +67,7 @@ async function handleInit( directory?: string, encryptionKey?: Uint8Array, ): Promise { - sqlite3 ??= await sqlite3InitModule(); - const s = sqlite3; + const s = (sqlite3 ??= await sqlite3InitModule()); if (encryptionKey !== undefined && ephemeral) { throw new SqliteEncryptionError( 'encryption_not_supported_for_ephemeral', @@ -79,13 +77,14 @@ async function handleInit( if (ephemeral) { db = new s.oo1.DB(':memory:', 'c'); } else { - await ensurePool(directory ?? DEFAULT_SAH_POOL_DIRECTORY); + const activePool = await ensurePool(directory ?? DEFAULT_SAH_POOL_DIRECTORY); dbPath = normalizeDbPath(dbName); if (encryptionKey !== undefined) { - db = new s.oo1.DB({ filename: dbPath, flags: 'c', vfs: MC_SAH_POOL_VFS_NAME }); - applyEncryptionKey(db, encryptionKey); + const conn = new s.oo1.DB({ filename: dbPath, flags: 'c', vfs: MC_SAH_POOL_VFS_NAME }); + db = conn; + applyEncryptionKey(conn, encryptionKey); } else { - db = new pool!.OpfsSAHPoolDb(dbPath); + db = new activePool.OpfsSAHPoolDb(dbPath); } } runSql(SCHEMA_SQL); diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 8d34baa72736..af582042e526 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -1,19 +1,20 @@ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; -import { type BlockHash, type L2BlockTag, L2TipsStoreBase } from '@aztec/stdlib/block'; -import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; +import { type BlockHash, type CheckpointId, type L2BlockTag, L2TipsStoreBase } from '@aztec/stdlib/block'; import type { AztecAsyncMap } from '../interfaces/map.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; +/** Serialized form of a per-tip checkpoint id stored in the KV store. */ +type StoredCheckpointId = { number: number; hash: string }; + /** * Persistent implementation of L2 tips store backed by a KV store. * Used by nodes that need to persist chain state across restarts. */ export class L2TipsKVStore extends L2TipsStoreBase { private readonly l2TipsStore: AztecAsyncMap; + private readonly l2TipCheckpointsStore: AztecAsyncMap; private readonly l2BlockHashesStore: AztecAsyncMap; - private readonly l2BlockNumberToCheckpointNumberStore: AztecAsyncMap; - private readonly l2CheckpointStore: AztecAsyncMap; constructor( private store: AztecAsyncKVStore, @@ -22,11 +23,8 @@ export class L2TipsKVStore extends L2TipsStoreBase { ) { super(initialBlockHash); this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_')); + this.l2TipCheckpointsStore = store.openMap([namespace, 'l2_tip_checkpoints'].join('_')); this.l2BlockHashesStore = store.openMap([namespace, 'l2_block_hashes'].join('_')); - this.l2BlockNumberToCheckpointNumberStore = store.openMap( - [namespace, 'l2_block_number_to_checkpoint_number'].join('_'), - ); - this.l2CheckpointStore = store.openMap([namespace, 'l2_checkpoint_store'].join('_')); } protected getTip(tag: L2BlockTag): Promise { @@ -37,6 +35,18 @@ export class L2TipsKVStore extends L2TipsStoreBase { return this.l2TipsStore.set(tag, blockNumber); } + protected async getTipCheckpoint(tag: L2BlockTag): Promise { + const stored = await this.l2TipCheckpointsStore.getAsync(tag); + if (stored === undefined) { + return undefined; + } + return { number: CheckpointNumber(stored.number), hash: stored.hash }; + } + + protected setTipCheckpoint(tag: L2BlockTag, checkpoint: CheckpointId): Promise { + return this.l2TipCheckpointsStore.set(tag, { number: checkpoint.number, hash: checkpoint.hash }); + } + protected getStoredBlockHash(blockNumber: BlockNumber): Promise { return this.l2BlockHashesStore.getAsync(blockNumber); } @@ -51,38 +61,6 @@ export class L2TipsKVStore extends L2TipsStoreBase { } } - protected getCheckpointNumberForBlock(blockNumber: BlockNumber): Promise { - return this.l2BlockNumberToCheckpointNumberStore.getAsync(blockNumber); - } - - protected setCheckpointNumberForBlock(blockNumber: BlockNumber, checkpointNumber: CheckpointNumber): Promise { - return this.l2BlockNumberToCheckpointNumberStore.set(blockNumber, checkpointNumber); - } - - protected async deleteBlockToCheckpointBefore(blockNumber: BlockNumber): Promise { - for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: blockNumber })) { - await this.l2BlockNumberToCheckpointNumberStore.delete(key); - } - } - - protected async getCheckpoint(checkpointNumber: CheckpointNumber): Promise { - const buffer = await this.l2CheckpointStore.getAsync(checkpointNumber); - if (!buffer) { - return undefined; - } - return PublishedCheckpoint.fromBuffer(buffer); - } - - protected saveCheckpointData(checkpoint: PublishedCheckpoint): Promise { - return this.l2CheckpointStore.set(checkpoint.checkpoint.number, checkpoint.toBuffer()); - } - - protected async deleteCheckpointsBefore(checkpointNumber: CheckpointNumber): Promise { - for await (const key of this.l2CheckpointStore.keysAsync({ end: checkpointNumber })) { - await this.l2CheckpointStore.delete(key); - } - } - protected runInTransaction(fn: () => Promise): Promise { return this.store.transactionAsync(fn); } diff --git a/yarn-project/p2p/README.md b/yarn-project/p2p/README.md index 1ed930c351de..4ae26bc229c9 100644 --- a/yarn-project/p2p/README.md +++ b/yarn-project/p2p/README.md @@ -116,7 +116,10 @@ Per-peer limit exceeded: `HighToleranceError` + `RATE_LIMIT_EXCEEDED` status. Gl |-------|-------|--------| | > -50 | Healthy | Normal | | -100 < score <= -50 | Disconnect | GOODBYE sent + disconnect on next heartbeat | -| <= -100 | Banned | GOODBYE sent + disconnect on next heartbeat | +| <= -100 | Banned | GOODBYE sent + disconnect on next heartbeat; banned for `P2P_PEER_BAN_DURATION_SECONDS` (default 24h) | + +Once a peer is banned its score is pinned at the ban level for the configured duration (it does not decay-recover), +and only lifts when the window expires. See [Gossipsub Scoring](src/services/gossipsub/README.md#ban-duration) for details. ### Protocol Summary diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index a331fae469c1..6b4306b061f6 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -7,7 +7,7 @@ import { AztecLMDBStoreV2, createStore } from '@aztec/kv-store/lmdb-v2'; import type { BlockHash, L2BlockSource } from '@aztec/stdlib/block'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { ContractDataSource } from '@aztec/stdlib/contract'; -import type { BlockMinFeesProvider } from '@aztec/stdlib/gas'; +import { type BlockMinFeesProvider, getNetworkTxGasLimits } from '@aztec/stdlib/gas'; import type { AztecNode, ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -75,8 +75,10 @@ export async function createP2PClient( } const bindings = logger.getBindings(); - // Schema version 3: tx proofs are stored in a separate map from the tx data (see TxPoolV2Impl). - const store = deps.store ?? (await createStore(P2P_STORE_NAME, 3, config, bindings)); + // Schema version 4: L2 tips store resolves checkpoint tips from per-tip ids in l2_tip_checkpoints; the + // block->checkpoint mapping and checkpoint maps were dropped. Bumped to wipe stores whose tips predate + // per-tip ids, which would otherwise make getL2Tips throw on every read. + const store = deps.store ?? (await createStore(P2P_STORE_NAME, 4, config, bindings)); const archive = await createStore(P2P_ARCHIVE_STORE_NAME, 1, config, bindings); const peerStore = await createStore(P2P_PEER_STORE_NAME, 1, config, bindings); const attestationStore = await createStore(P2P_ATTESTATION_STORE_NAME, 2, config, bindings); @@ -109,14 +111,14 @@ export async function createP2PClient( const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); const l1Constants = await archiver.getL1Constants(); const gasFees = await blockMinFeesProvider.getCurrentMinFees(); + const networkTxGasLimits = getNetworkTxGasLimits(config, l1Constants); return createTxValidatorForTransactionsEnteringPendingTxPool( worldStateSynchronizer, nextSlotTimestamp, BlockNumber(currentBlockNumber + 1), { - rollupManaLimit: l1Constants.rollupManaLimit, - maxBlockL2Gas: config.validateMaxL2BlockGas, - maxBlockDAGas: config.validateMaxDABlockGas, + maxTxL2Gas: networkTxGasLimits.l2Gas, + maxTxDAGas: networkTxGasLimits.daGas, }, gasFees, ); diff --git a/yarn-project/p2p/src/client/p2p_client.test.ts b/yarn-project/p2p/src/client/p2p_client.test.ts index 01c03e0d206d..2d2e10aa6425 100644 --- a/yarn-project/p2p/src/client/p2p_client.test.ts +++ b/yarn-project/p2p/src/client/p2p_client.test.ts @@ -335,10 +335,6 @@ describe('P2P Client', () => { await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(100), hash: expect.any(String) }, checkpointed: { block: { number: BlockNumber(100), hash: expect.any(String) }, checkpoint: anyCheckpoint }, - proposedCheckpoint: { - block: { number: BlockNumber(100), hash: expect.any(String) }, - checkpoint: anyCheckpoint, - }, proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, }); @@ -349,7 +345,6 @@ describe('P2P Client', () => { await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(90), hash: expect.any(String) }, - proposedCheckpoint: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, checkpointed: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, @@ -362,7 +357,6 @@ describe('P2P Client', () => { await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(92), hash: expect.any(String) }, - proposedCheckpoint: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: anyCheckpoint }, checkpointed: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: anyCheckpoint }, proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, @@ -404,6 +398,43 @@ describe('P2P Client', () => { expect(await client.getSyncedLatestBlockNum()).toEqual(102); }); + it('prepares the pool for the last synced block slot after marking txs mined', async () => { + await client.start(); + + blockSource.addProposedBlocks([ + await L2Block.random(BlockNumber(101), { slotNumber: SlotNumber(150) }), + await L2Block.random(BlockNumber(102), { slotNumber: SlotNumber(151) }), + ]); + await client.sync(); + + // Release is driven by the synced block slot, not the wall clock: prepareForSlot is ultimately + // driven with the last synced block's slot (regardless of how the stream batches the blocks), + // never reads the epoch cache, and never targets a slot beyond what has synced. + expect(txPool.prepareForSlot).toHaveBeenLastCalledWith(SlotNumber(151)); + const preparedSlots = txPool.prepareForSlot.mock.calls.map(([slot]) => Number(slot)); + expect(Math.max(...preparedSlots)).toBe(151); + expect(epochCache.getCurrentAndNextSlot).not.toHaveBeenCalled(); + // Mined-marking runs before the matching-slot release within the handler. + expect(txPool.handleMinedBlock.mock.invocationCallOrder.at(-1)!).toBeLessThan( + txPool.prepareForSlot.mock.invocationCallOrder.at(-1)!, + ); + }); + + it('does not re-prepare for a slot that does not advance', async () => { + await client.start(); + + blockSource.addProposedBlocks([await L2Block.random(BlockNumber(101), { slotNumber: SlotNumber(150) })]); + await client.sync(); + const callsAfterFirst = txPool.prepareForSlot.mock.calls.length; + expect(txPool.prepareForSlot).toHaveBeenLastCalledWith(SlotNumber(150)); + + // A later block at an earlier slot must not advance the prepared-for slot + blockSource.addProposedBlocks([await L2Block.random(BlockNumber(102), { slotNumber: SlotNumber(149) })]); + await client.sync(); + expect(txPool.prepareForSlot.mock.calls.length).toBe(callsAfterFirst); + expect(txPool.prepareForSlot).not.toHaveBeenCalledWith(SlotNumber(149)); + }); + it('handles proven and finalized chain behind starting point', async () => { blockSource.setProvenBlockNumber(0); blockSource.setFinalizedBlockNumber(0); diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 2d07cfd79c92..4d959a044e6b 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -6,7 +6,6 @@ import { SlotNumber, } from '@aztec/foundation/branded-types'; import { createLogger } from '@aztec/foundation/log'; -import { RunningPromise } from '@aztec/foundation/promise'; import { DateProvider } from '@aztec/foundation/timer'; import type { AztecAsyncKVStore, AztecAsyncSingleton } from '@aztec/kv-store'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; @@ -20,8 +19,8 @@ import { type L2BlockSource, L2BlockStream, type L2BlockStreamEvent, - type L2Tips, type L2TipsStore, + type LocalL2Tips, } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; @@ -83,9 +82,6 @@ export class P2PClient extends WithTracer implements P2P { /** Tracks the last slot for which we called prepareForSlot */ private lastSlotProcessed: SlotNumber = SlotNumber.ZERO; - /** Polls for slot changes and calls prepareForSlot on the tx pool */ - private slotMonitor: RunningPromise | undefined; - constructor( private store: AztecAsyncKVStore, private l2BlockSource: L2BlockSource & ContractDataSource, @@ -152,7 +148,7 @@ export class P2PClient extends WithTracer implements P2P { this.p2pService.updateConfig(config); } - public getL2Tips(): Promise { + public getL2Tips(): Promise { return this.l2Tips.getL2Tips(); } @@ -178,7 +174,7 @@ export class P2PClient extends WithTracer implements P2P { break; case 'chain-pruned': this.txCollection.stopCollectingForBlocksAfter(event.block.number); - await this.handlePruneL2Blocks(event.block, event.checkpoint); + await this.handlePruneL2Blocks(event.block, event.checkpointed.checkpoint); break; case 'chain-checkpointed': break; @@ -268,14 +264,6 @@ export class P2PClient extends WithTracer implements P2P { this.blockStream.start(); this.txFileStore?.start(); - // Start slot monitor to call prepareForSlot when the slot changes - this.slotMonitor = new RunningPromise( - () => this.maybeCallPrepareForSlot(), - this.log, - this.config.slotCheckIntervalMS, - ); - this.slotMonitor.start(); - return this.syncPromise; } @@ -302,8 +290,6 @@ export class P2PClient extends WithTracer implements P2P { */ public async stop() { this.log.debug('Stopping p2p client...'); - await this.slotMonitor?.stop(); - this.log.debug('Stopped slot monitor'); await tryStop(this.txCollection); this.log.debug('Stopped tx collection service'); await this.txFileStore?.stop(); @@ -642,11 +628,16 @@ export class P2PClient extends WithTracer implements P2P { return; } + const lastBlock = blocks.at(-1)!; + const lastSlot = lastBlock.header.getSlot(); + + // Mark txs mined before releasing protections: a block landing at this slot supersedes any + // protection for txs it includes, so mined-marking must run first to keep just-landed txs from + // being unprotected into pending where they could be evicted before they are recorded as mined. await this.handleMinedBlocks(blocks); - await this.maybeCallPrepareForSlot(); + await this.maybeCallPrepareForSlot(lastSlot); await this.collectingMissingTxs(blocks); - const lastBlock = blocks.at(-1)!; - await this.synchedLatestSlot.set(BigInt(lastBlock.header.getSlot())); + await this.synchedLatestSlot.set(BigInt(lastSlot)); } /** Request txs for unproven blocks so the prover node can prove. */ @@ -741,20 +732,8 @@ export class P2PClient extends WithTracer implements P2P { return isEpochPrune; } - /** Checks if the slot has changed and calls prepareForSlot if so. */ - private async maybeCallPrepareForSlot(): Promise { - // If we have a proposed checkpoint available, we want to prepare the target slot - otherwise we prepare the current slot - const l2Tips = await this.l2Tips.getL2Tips(); - const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number; - - let slot; - if (hasProposedCheckpoint) { - const { targetSlot } = this.epochCache.getTargetAndNextSlot(); - slot = targetSlot; - } else { - const { currentSlot } = this.epochCache.getCurrentAndNextSlot(); - slot = currentSlot; - } + /** Calls prepareForSlot for the given slot if it advances past the last slot we prepared for. */ + private async maybeCallPrepareForSlot(slot: SlotNumber): Promise { if (slot <= this.lastSlotProcessed) { return; } diff --git a/yarn-project/p2p/src/config.ts b/yarn-project/p2p/src/config.ts index 658f630f3af5..2878e65157ce 100644 --- a/yarn-project/p2p/src/config.ts +++ b/yarn-project/p2p/src/config.ts @@ -42,7 +42,6 @@ export interface P2PConfig TxFileStoreConfig, Pick< SequencerConfig, - | 'blockDurationMs' | 'expectedBlockProposalsPerSlot' | 'maxTxsPerBlock' | 'attestationPropagationTime' @@ -50,28 +49,22 @@ export interface P2PConfig | 'checkpointProposalSyncGraceSeconds' | 'minBlockDuration' | 'maxBlocksPerCheckpoint' - > { + >, + // `blockDurationMs` is optional on the loose `SequencerConfig` but is always populated for p2p via + // the shared `numberConfigHelper(3000)` mapping, so it is required here. + Required> { /** Maximum transactions per block for validation. Overrides maxTxsPerBlock for gossip validation when set. */ validateMaxTxsPerBlock?: number; /** Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set. */ validateMaxTxsPerCheckpoint?: number; - /** Maximum L2 gas per block for validation. When set, txs exceeding this limit are rejected. */ - validateMaxL2BlockGas?: number; - - /** Maximum DA gas per block for validation. When set, txs exceeding this limit are rejected. */ - validateMaxDABlockGas?: number; - /** A flag dictating whether the P2P subsystem should be enabled. */ p2pEnabled: boolean; /** The frequency in which to check for new L2 blocks. */ blockCheckIntervalMS: number; - /** The frequency in which to check for new L2 slots. */ - slotCheckIntervalMS: number; - /** The number of blocks to fetch in a single batch. */ blockRequestBatchSize: number; @@ -174,6 +167,9 @@ export interface P2PConfig /** The values for the peer scoring system. Passed as a comma separated list of values in order: low, mid, high tolerance errors. */ peerPenaltyValues: number[]; + /** How long (in seconds) a peer is banned for once its score drops below the ban threshold. */ + peerBanDurationSeconds: number; + /** Limit of transactions to archive in the tx pool. Once the archived tx limit is reached, the oldest archived txs will be purged. */ archivedTxLimit: number; @@ -287,16 +283,6 @@ export const p2pConfigMappings: ConfigMappingsType = { 'Maximum transactions per checkpoint for validation. Used as fallback for maxTxsPerBlock when that is not set.', ...optionalNumberConfigHelper(), }, - validateMaxL2BlockGas: { - env: 'VALIDATOR_MAX_L2_BLOCK_GAS', - description: 'Maximum L2 gas per block for validation. When set, txs exceeding this limit are rejected.', - ...optionalNumberConfigHelper(), - }, - validateMaxDABlockGas: { - env: 'VALIDATOR_MAX_DA_BLOCK_GAS', - description: 'Maximum DA gas per block for validation. When set, txs exceeding this limit are rejected.', - ...optionalNumberConfigHelper(), - }, p2pEnabled: { env: 'P2P_ENABLED', description: 'A flag dictating whether the P2P subsystem should be enabled.', @@ -312,11 +298,6 @@ export const p2pConfigMappings: ConfigMappingsType = { description: 'The frequency in which to check for new L2 blocks.', ...numberConfigHelper(100), }, - slotCheckIntervalMS: { - env: 'P2P_SLOT_CHECK_INTERVAL_MS', - description: 'The frequency in which to check for new L2 slots.', - ...numberConfigHelper(1000), - }, debugDisableColocationPenalty: { env: 'DEBUG_P2P_DISABLE_COLOCATION_PENALTY', description: 'DEBUG: Disable colocation penalty - NEVER set to true in production', @@ -476,6 +457,11 @@ export const p2pConfigMappings: ConfigMappingsType = { 'The values for the peer scoring system. Passed as a comma separated list of values in order: low, mid, high tolerance errors.', defaultValue: [2, 10, 50], }, + peerBanDurationSeconds: { + env: 'P2P_PEER_BAN_DURATION_SECONDS', + description: 'How long (in seconds) a peer is banned for once its score drops below the ban threshold.', + ...numberConfigHelper(24 * 60 * 60), + }, doubleSpendSeverePeerPenaltyWindow: { env: 'P2P_DOUBLE_SPEND_SEVERE_PEER_PENALTY_WINDOW', description: 'The "age" (in L2 blocks) of a tx after which we heavily penalize a peer for sending it.', diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts index c49f2fce3805..2f7fab151b30 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts @@ -170,6 +170,17 @@ export interface TxPoolV2 extends TypedEventEmitter { */ prepareForSlot(slotNumber: SlotNumber): Promise; + /** + * Releases the protections a failed block proposal created and restores the txs to pending. + * Only clears protection entries still recorded at exactly the given slot: a tx that another, + * still-live proposal raised to a higher slot via {@link protectTxs} keeps its protection, and + * mined txs (which carry no protection entry) are left untouched. Restored txs are re-validated + * and resolved against nullifier conflicts before re-entering the pending indices. + * @param txHashes - Hashes of the proposal's txs to release. + * @param slotNumber - The slot the failed proposal targeted; protection is released only for this slot. + */ + unprotectTxs(txHashes: TxHash[], slotNumber: SlotNumber): Promise; + /** * Handles pruned blocks during a reorg. * Un-mines all transactions mined in blocks beyond the given latest block diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts index f39a4c4e5a09..d0dcc8200564 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_indices.ts @@ -131,6 +131,9 @@ export class TxPoolIndices { meta.minedL2BlockId = blockId; // Safe to call unconditionally - removeFromPendingIndices is idempotent this.#removeFromPendingIndices(meta); + // A mined tx supersedes any protection: drop the stale entry so it can't linger in the map and + // be matched by later protection scans. + this.#protectedTransactions.delete(meta.txHash); } /** Clears the mined status from a transaction */ @@ -316,6 +319,15 @@ export class TxPoolIndices { return result; } + /** + * From the given hashes, returns those whose protection is recorded at exactly the given slot. + * Used to release the protections a single block proposal created without disturbing entries a + * later proposal raised to a higher slot via updateProtection. + */ + findProtectedTxsAtSlot(txHashes: string[], slotNumber: SlotNumber): string[] { + return txHashes.filter(txHash => this.#protectedTransactions.get(txHash) === slotNumber); + } + /** Filters out transactions that are currently protected */ filterUnprotected(txs: TxMetaData[]): TxMetaData[] { return txs.filter(meta => !this.#protectedTransactions.has(meta.txHash)); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts index a7bde8270d3a..53f0ec8d9be5 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts @@ -14,7 +14,7 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { RevertCode } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Body, L2Block, type L2BlockId, type L2BlockSource } from '@aztec/stdlib/block'; -import { FALLBACK_TEARDOWN_DA_GAS_LIMIT, Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { mockTx } from '@aztec/stdlib/testing'; import { @@ -38,7 +38,14 @@ type MockTx = Awaited>; // Default maxFeesPerGas used by mockTx is GasFees(10, 10). const DEFAULT_MAX_FEES_PER_GAS = new GasFees(10, 10); -const DEFAULT_TX_FEE_LIMIT = GasSettings.fallback({ maxFeesPerGas: DEFAULT_MAX_FEES_PER_GAS }).getFeeLimit().toBigInt(); +const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); +const TEARDOWN_DA_GAS = 98_304; +const DEFAULT_TX_FEE_LIMIT = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: DEFAULT_MAX_FEES_PER_GAS, +}) + .getFeeLimit() + .toBigInt(); /** A validator that accepts all transactions. Used in tests that don't need validation. */ const alwaysValidValidator: TxValidator = { @@ -799,7 +806,7 @@ describe('TxPoolV2', () => { tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: DEFAULT_MAX_FEES_PER_GAS, - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); const result = await gasPool.addPendingTxs([tx]); expect(result.accepted).toHaveLength(0); @@ -811,7 +818,7 @@ describe('TxPoolV2', () => { tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: DEFAULT_MAX_FEES_PER_GAS, - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); const result = await gasPool.addPendingTxs([tx]); expect(result.accepted).toHaveLength(0); @@ -1515,6 +1522,23 @@ describe('TxPoolV2', () => { expectNoCallbacks(); // State transition only, tx not removed from pool }); + it('a tx protected at an earlier slot stays mined when its block lands, not unprotected to pending', async () => { + // Models the event-driven release: the block stream handler marks txs mined for the landed + // block before releasing protections from earlier slots. A tx protected at slot 1 that lands + // in a block at slot 2 must end up mined, never bounced through pending where it could be lost. + const tx = await mockTx(1); + await pool.addProtectedTxs([tx], slot1Header); + expectAddedTxs(tx); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + + // Mark mined first (as the blocks-added handler does), then release earlier-slot protections. + await pool.handleMinedBlock(makeBlock([tx], slot2Header)); + await pool.prepareForSlot(SlotNumber(2)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('mined'); + expect(await pool.getPendingTxCount()).toBe(0); + }); + it('handles empty block gracefully', async () => { // Should not throw when processing an empty block await pool.handleMinedBlock(makeEmptyBlock(slot1Header)); @@ -1800,6 +1824,97 @@ describe('TxPoolV2', () => { }); }); + describe('unprotectTxs', () => { + it('restores matching-slot protected txs to pending', async () => { + const tx = await mockTx(1); + await pool.addProtectedTxs([tx], slot1Header); + expectAddedTxs(tx); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + + await pool.unprotectTxs([tx.getTxHash()], SlotNumber(1)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('pending'); + expect(await pool.getPendingTxCount()).toBe(1); + expectNoCallbacks(); // state transition only, tx not added or removed + }); + + it('leaves protections recorded at a later slot untouched', async () => { + const tx = await mockTx(1); + // Protected for slot 1, then raised to slot 2 by a second live proposal + await pool.addProtectedTxs([tx], slot1Header); + expectAddedTxs(tx); + await pool.addProtectedTxs([tx], slot2Header); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + + // The failed slot-1 proposal releases its protection, but the slot-2 protection survives + await pool.unprotectTxs([tx.getTxHash()], SlotNumber(1)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + expectNoCallbacks(); + }); + + it('only releases the requested hashes that match the slot', async () => { + const txReleased = await mockTx(1); + const txKeptLaterSlot = await mockTx(2); + const txUnrelated = await mockTx(3); + + await pool.addProtectedTxs([txReleased], slot1Header); + await pool.addProtectedTxs([txKeptLaterSlot], slot2Header); + await pool.addProtectedTxs([txUnrelated], slot1Header); + clearCallbackTracking(); + + // Failed proposal at slot 1 referenced only txReleased and txKeptLaterSlot + await pool.unprotectTxs([txReleased.getTxHash(), txKeptLaterSlot.getTxHash()], SlotNumber(1)); + + expect(await pool.getTxStatus(txReleased.getTxHash())).toBe('pending'); + expect(await pool.getTxStatus(txKeptLaterSlot.getTxHash())).toBe('protected'); // slot 2, not released + expect(await pool.getTxStatus(txUnrelated.getTxHash())).toBe('protected'); // not in the hash list + }); + + it('leaves mined txs untouched', async () => { + const tx = await mockTx(1); + // Protected, then mined at slot 1 (mining clears the protection entry) + await pool.addProtectedTxs([tx], slot1Header); + await pool.handleMinedBlock(makeBlock([tx], slot1Header)); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('mined'); + clearCallbackTracking(); + + await pool.unprotectTxs([tx.getTxHash()], SlotNumber(1)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('mined'); + expect(await pool.getPendingTxCount()).toBe(0); + expectNoCallbacks(); + }); + + it('is a no-op when no protection matches the slot', async () => { + const tx = await mockTx(1); + await pool.addProtectedTxs([tx], slot2Header); + clearCallbackTracking(); + + await pool.unprotectTxs([tx.getTxHash()], SlotNumber(1)); + + expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + expectNoCallbacks(); + }); + + it('resolves nullifier conflicts when restoring to pending', async () => { + const txPending = await mockPublicTx(1, 5); + const txProtected = await mockPublicTx(2, 20); + // Protected tx shares a nullifier with the pending tx but has higher priority + setNullifier(txProtected, 0, getNullifier(txPending, 0)); + + await pool.addPendingTxs([txPending]); + await pool.addProtectedTxs([txProtected], slot1Header); + clearCallbackTracking(); + + await pool.unprotectTxs([txProtected.getTxHash()], SlotNumber(1)); + + // Higher-priority unprotected tx wins the conflict; the pending loser is evicted + expect(await pool.getTxStatus(txProtected.getTxHash())).toBe('pending'); + expect(await pool.getTxStatus(txPending.getTxHash())).toBe('deleted'); + }); + }); + describe('handlePrunedBlocks', () => { it('un-mines transactions from pruned block', async () => { const tx = await mockTx(1); @@ -2108,6 +2223,23 @@ describe('TxPoolV2', () => { expect(await poolWithValidator.getPendingTxCount()).toBe(0); }); + it('unprotectTxs deletes tx that fails validation when restoring', async () => { + const tx = await mockTx(1); + + await poolWithValidator.addProtectedTxs([tx], slot1Header); + expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('protected'); + + mockValidator.validateTx.mockResolvedValue({ + result: 'invalid', + reason: ['tx expired'], + }); + + await poolWithValidator.unprotectTxs([tx.getTxHash()], SlotNumber(1)); + + expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('deleted'); + expect(await poolWithValidator.getPendingTxCount()).toBe(0); + }); + it('prepareForSlot keeps tx that passes validation when unprotecting', async () => { const tx = await mockTx(1); @@ -2783,60 +2915,50 @@ describe('TxPoolV2', () => { }); describe('protected tx in pruned block', () => { - it('protected tx during prune that later fails validation should be soft-deleted', async () => { + it('mined tx from pruned block that fails revalidation on un-mine should be soft-deleted', async () => { const tx = await mockTx(1); - // Add, protect, and mine the tx + // Add, protect, and mine the tx. Mining clears the protection entry. await poolWithValidator.addPendingTxs([tx]); await poolWithValidator.addProtectedTxs([tx], slot1Header); await poolWithValidator.handleMinedBlock(makeBlock([tx], slot1Header)); expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('mined'); - // Prune - tx is un-mined but stays protected (validator passes at this point) - await poolWithValidator.handlePrunedBlocks(block0Id); - expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('protected'); - - // Now make validator reject this tx + // Make validator reject this tx, then prune. handlePrunedBlocks revalidates un-mined txs. mockValidator.validateTx.mockResolvedValue({ result: 'invalid', reason: ['timestamp expired'], }); - - // Unprotect (prepareForSlot) - tx fails validation - await poolWithValidator.prepareForSlot(SlotNumber(2)); + await poolWithValidator.handlePrunedBlocks(block0Id); // The tx was in a pruned block, so it should be SOFT-deleted, not hard-deleted expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('deleted'); expect(await poolWithValidator.getTxByHash(tx.getTxHash())).toBeDefined(); }); - it('protected tx during prune that later loses nullifier conflict should be soft-deleted', async () => { - const txProtected = await mockPublicTx(1, 5); + it('mined tx from pruned block that loses nullifier conflict on un-mine should be soft-deleted', async () => { + const txMined = await mockPublicTx(1, 5); const txHigherPriority = await mockPublicTx(2, 10); // Give them the same nullifier - setNullifier(txHigherPriority, 0, getNullifier(txProtected, 0)); + setNullifier(txHigherPriority, 0, getNullifier(txMined, 0)); - // Add, protect, and mine txProtected - await poolWithValidator.addPendingTxs([txProtected]); - await poolWithValidator.addProtectedTxs([txProtected], slot1Header); - await poolWithValidator.handleMinedBlock(makeBlock([txProtected], slot1Header)); - expect(await poolWithValidator.getTxStatus(txProtected.getTxHash())).toBe('mined'); + // Add, protect, and mine txMined. Mining clears the protection entry. + await poolWithValidator.addPendingTxs([txMined]); + await poolWithValidator.addProtectedTxs([txMined], slot1Header); + await poolWithValidator.handleMinedBlock(makeBlock([txMined], slot1Header)); + expect(await poolWithValidator.getTxStatus(txMined.getTxHash())).toBe('mined'); - // Prune - txProtected is un-mined but stays protected - await poolWithValidator.handlePrunedBlocks(block0Id); - expect(await poolWithValidator.getTxStatus(txProtected.getTxHash())).toBe('protected'); - - // Now add a higher priority tx with same nullifier + // Add a higher priority pending tx with the same nullifier await poolWithValidator.addPendingTxs([txHigherPriority]); expect(await poolWithValidator.getTxStatus(txHigherPriority.getTxHash())).toBe('pending'); - // Unprotect (prepareForSlot) - txProtected loses nullifier conflict - await poolWithValidator.prepareForSlot(SlotNumber(2)); + // Prune - txMined is un-mined and loses the nullifier conflict during handlePrunedBlocks + await poolWithValidator.handlePrunedBlocks(block0Id); // The tx was in a pruned block, so it should be SOFT-deleted, not hard-deleted - expect(await poolWithValidator.getTxStatus(txProtected.getTxHash())).toBe('deleted'); - expect(await poolWithValidator.getTxByHash(txProtected.getTxHash())).toBeDefined(); + expect(await poolWithValidator.getTxStatus(txMined.getTxHash())).toBe('deleted'); + expect(await poolWithValidator.getTxByHash(txMined.getTxHash())).toBeDefined(); // Higher priority tx should be pending expect(await poolWithValidator.getTxStatus(txHigherPriority.getTxHash())).toBe('pending'); @@ -2971,7 +3093,7 @@ describe('TxPoolV2', () => { expect(await pool.getTxStatus(tx.getTxHash())).toBe('pending'); }); - it('pending -> protected -> mined -> protected (reorg, still valid)', async () => { + it('pending -> protected -> mined -> pending (reorg, still valid)', async () => { const tx = await mockTx(1); await pool.addPendingTxs([tx]); @@ -2982,10 +3104,11 @@ describe('TxPoolV2', () => { expectNoCallbacks(); expect(await pool.getTxStatus(tx.getTxHash())).toBe('mined'); - // After reorg, tx retains its protection status (protection is managed by prepareForSlot) + // Mining supersedes protection and clears its entry, so a later reorg un-mines the tx back to + // pending (not protected). Event-driven release then handles it like any other pending tx. await pool.handlePrunedBlocks(block0Id); expectNoCallbacks(); // State transition only - expect(await pool.getTxStatus(tx.getTxHash())).toBe('protected'); + expect(await pool.getTxStatus(tx.getTxHash())).toBe('pending'); }); it('N/A -> protected -> mined -> deleted (req/resp flow)', async () => { @@ -4373,6 +4496,7 @@ describe('TxPoolV2', () => { // Default gas limits are ~1e7 each, so with maxFees of 1e12 we get ~1e19 fee limit const highFeeTx = await mockTx(4, { numberOfNonRevertiblePublicCallRequests: 1 }); highFeeTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, maxFeesPerGas: new GasFees(1e12, 1e12), }); @@ -5802,7 +5926,7 @@ describe('TxPoolV2', () => { const makeTxWithMaxFees = async (seed: number, maxFeesPerGas: GasFees) => { const tx = await mockTx(seed, { numberOfNonRevertiblePublicCallRequests: 1 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: DEFAULT_GAS_LIMITS, maxFeesPerGas }); return tx; }; @@ -5886,6 +6010,7 @@ describe('TxPoolV2', () => { maxPriorityFeesPerGas: new GasFees(1, 1), }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, maxFeesPerGas, maxPriorityFeesPerGas: new GasFees(1, 1), }); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts index f3ab87678659..9fc60e7c2c3b 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts @@ -110,6 +110,10 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte return this.#queue.put(() => this.#impl.prepareForSlot(slotNumber)); } + unprotectTxs(txHashes: TxHash[], slotNumber: SlotNumber): Promise { + return this.#queue.put(() => this.#impl.unprotectTxs(txHashes, slotNumber)); + } + handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise { return this.#queue.put(() => this.#impl.handlePrunedBlocks(latestBlock, options)); } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts index 90053a0fb3e6..8cfb4a7c24ab 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts @@ -585,29 +585,62 @@ export class TxPoolV2Impl { } this.#log.info(`Preparing for slot ${slotNumber}: unprotecting ${txsToRestore.length} txs`); + await this.#restoreUnprotectedToPending(txsToRestore, 'during prepareForSlot'); + }); + } - // Step 4: Validate for pending pool - const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, 'during prepareForSlot'); + async unprotectTxs(txHashes: TxHash[], slotNumber: SlotNumber): Promise { + const hashStrs = txHashes.map(h => h.toString()); - // Step 5: Resolve nullifier conflicts and add winners to pending indices - const { added, toEvict } = this.#applyNullifierConflictResolution(valid); + await this.#store.transactionAsync(async () => { + // Only release entries still recorded at this exact slot. A tx that another, still-live proposal + // raised to a higher slot via updateProtection must keep that protection. Mined txs have no + // protection entry and so are never matched here. + const matching = this.#indices.findProtectedTxsAtSlot(hashStrs, slotNumber); + if (matching.length === 0) { + this.#log.debug(`Unprotecting txs for slot ${slotNumber}: no matching protections to release`); + return; + } - // Step 6: Delete invalid txs and evict conflict losers - await this.#deleteTxsBatch(invalid); - await this.#evictTxs(toEvict, 'NullifierConflict'); + this.#indices.clearProtection(matching); - // Step 7: Run eviction rules (enforce pool size limit) - if (added.length > 0) { - const feePayers = added.map(meta => meta.feePayer); - const uniqueFeePayers = new Set(feePayers); - await this.#evictionManager.evictAfterNewTxs( - added.map(m => m.txHash), - [...uniqueFeePayers], - ); + const txsToRestore = this.#indices.filterRestorable(matching); + if (txsToRestore.length === 0) { + return; } + + this.#log.info(`Unprotecting ${txsToRestore.length} txs from failed proposal at slot ${slotNumber}`); + await this.#restoreUnprotectedToPending(txsToRestore, 'during unprotectTxs'); }); } + /** + * Returns just-unprotected txs to the pending pool: revalidates them, resolves nullifier + * conflicts, deletes losers, then enforces the pool size limit. Must run inside a store + * transaction; callers clear the protection entries before invoking. + */ + async #restoreUnprotectedToPending(txsToRestore: TxMetaData[], context: string): Promise { + // Step 1: Validate for pending pool + const { valid, invalid } = await this.#revalidateMetadata(txsToRestore, context); + + // Step 2: Resolve nullifier conflicts and add winners to pending indices + const { added, toEvict } = this.#applyNullifierConflictResolution(valid); + + // Step 3: Delete invalid txs and evict conflict losers + await this.#deleteTxsBatch(invalid); + await this.#evictTxs(toEvict, 'NullifierConflict'); + + // Step 4: Run eviction rules (enforce pool size limit) + if (added.length > 0) { + const feePayers = added.map(meta => meta.feePayer); + const uniqueFeePayers = new Set(feePayers); + await this.#evictionManager.evictAfterNewTxs( + added.map(m => m.txHash), + [...uniqueFeePayers], + ); + } + } + async handlePrunedBlocks(latestBlock: L2BlockId, options?: { deleteAllTxs?: boolean }): Promise { // Step 1: Find transactions mined after the prune point const txsToUnmine = this.#indices.findTxsMinedAfter(latestBlock.number); diff --git a/yarn-project/p2p/src/msg_validators/proposal_validator/proposal_validator.test.ts b/yarn-project/p2p/src/msg_validators/proposal_validator/proposal_validator.test.ts index f5e009765382..658c4bd0c156 100644 --- a/yarn-project/p2p/src/msg_validators/proposal_validator/proposal_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/proposal_validator/proposal_validator.test.ts @@ -25,10 +25,10 @@ import { ProposalValidator } from './proposal_validator.js'; const TEST_CLOCK_DISPARITY_MS = 500; /** Builds a multi-block timetable (S=72, E=12, D=6) matching the test's mocked l1 constants. */ -function makeTimetable(blockDurationMs: number | undefined = 6000) { +function makeTimetable(blockDurationMs = 6000) { return new ConsensusTimetable({ l1Constants: { l1GenesisTime: 0n, slotDuration: 72, ethereumSlotDuration: 12 }, - blockDuration: blockDurationMs !== undefined ? blockDurationMs / 1000 : undefined, + blockDuration: blockDurationMs / 1000, }); } @@ -371,38 +371,6 @@ describe('ProposalValidator', () => { const result = await validator.validate(proposal); expect(result).toEqual({ result: 'reject', severity: PeerErrorSeverity.HighToleranceError }); }); - - it('does not throw and accepts a checkpoint proposal in single-block mode (no blockDuration)', async () => { - // Single-block mode: blockDuration undefined → receive deadline drops the D term to - // target_slot_start - E = 7188s. Window [7116, 7188]; now = 7150 is inside. - validator = new ProposalValidator( - epochCache, - makeTimetable(undefined), - { - txsPermitted: true, - signatureContext: TEST_COORDINATION_SIGNATURE_CONTEXT, - clockDisparityMs: TEST_CLOCK_DISPARITY_MS, - }, - 'test', - ); - epochCache.getProposerAttesterAddressInSlot.mockResolvedValue(signer.address); - - const proposal = await makeCheckpointProposal({ - checkpointHeader: makeCheckpointHeader(0, { slotNumber: currentSlot }), - signer, - }); - - epochCache.getEpochAndSlotNow.mockReturnValue({ - epoch: EpochNumber(1), - slot: currentSlot, - ts: 7150n, - nowMs: 7150_000n, - }); - - // validate must not throw in single-block mode (the receive deadline drops the D term instead of - // calling the old requireBlockDuration); a thrown error would fail this await directly. - await expect(validator.validate(proposal)).resolves.toEqual({ result: 'accept' }); - }); }); describe('clock-disparity widening of the proposal receive window', () => { diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/README.md b/yarn-project/p2p/src/msg_validators/tx_validator/README.md index cfaa9b0aabb0..80365f8e5b7d 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -120,6 +120,8 @@ The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaD \* Gas balance check is skipped when `skipFeeEnforcement` is set (testing/dev). `GasTxValidator` internally delegates to `GasLimitsValidator` and `MaxFeePerGasValidator` as its first steps, so gas limits and fee-per-gas are checked wherever `GasTxValidator` runs. Pool migration uses `GasLimitsValidator` and `MaxFeePerGasValidator` standalone because it doesn't need the balance check. \** Proof verification is skipped for simulations (no verifier provided). +The gas-limit bounds `GasLimitsValidator` enforces here — the per-tx protocol maxima and the network admission limits — are documented in [`stdlib/src/gas/README.md`](../../../../stdlib/src/gas/README.md) under "Gas and Data Limits". + ## Fee-Per-Gas Rejection Strategy The `MaxFeePerGasValidator` and `InsufficientFeePerGasEvictionRule` reject and evict transactions whose `maxFeesPerGas` falls below the current block's gas fees. This is a simple strategy: if a tx can't pay the current fees, it gets rejected on entry and evicted after each new block. diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts index 20ba731ac7b6..3828826a0443 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts @@ -212,7 +212,6 @@ describe('Validator factory functions', () => { timestamp: 100n, blockNumber: BlockNumber(5), txsPermitted: true, - rollupManaLimit: Number.MAX_SAFE_INTEGER, }); const aggregate = validator as AggregateTxValidator; @@ -226,12 +225,13 @@ describe('Validator factory functions', () => { DoubleSpendTxValidator.name, DataTxValidator.name, ContractInstanceTxValidator.name, + GasLimitsValidator.name, GasTxValidator.name, TxProofValidator.name, ]); }); - it('excludes gas validator when fee enforcement is skipped', () => { + it('excludes the fee validator but keeps gas-limits validation when fee enforcement is skipped', () => { const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, proofVerifier, { l1ChainId: 1, rollupVersion: 2, @@ -241,15 +241,35 @@ describe('Validator factory functions', () => { timestamp: 100n, blockNumber: BlockNumber(5), txsPermitted: true, - rollupManaLimit: Number.MAX_SAFE_INTEGER, }); const aggregate = validator as AggregateTxValidator; const names = getValidatorNames(aggregate); + // Declared gas-limit admission is not fee enforcement, so it stays even with fees skipped. + expect(names).toContain(GasLimitsValidator.name); expect(names).not.toContain(GasTxValidator.name); expect(names).toContain(TxProofValidator.name); }); + it('excludes the gas-limits admission validator during simulation', () => { + // Gas estimation submits intentionally-inflated forEstimation limits, so the admission limit must not + // reject the estimation tx; the wallet clamps the real tx afterward. + const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { + l1ChainId: 1, + rollupVersion: 2, + setupAllowList: [], + gasFees: new GasFees(1, 1), + skipFeeEnforcement: true, + isSimulation: true, + timestamp: 100n, + blockNumber: BlockNumber(5), + txsPermitted: true, + }); + + const aggregate = validator as AggregateTxValidator; + expect(getValidatorNames(aggregate)).not.toContain(GasLimitsValidator.name); + }); + it('excludes proof validator when no verifier is provided', () => { const validator = createTxValidatorForAcceptingTxsOverRPC(db, contractSource, undefined, { l1ChainId: 1, @@ -259,7 +279,6 @@ describe('Validator factory functions', () => { timestamp: 100n, blockNumber: BlockNumber(5), txsPermitted: true, - rollupManaLimit: Number.MAX_SAFE_INTEGER, }); const aggregate = validator as AggregateTxValidator; @@ -308,7 +327,7 @@ describe('Validator factory functions', () => { synchronizer, 100n, BlockNumber(5), - { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + {}, new GasFees(1, 1), ); @@ -328,7 +347,7 @@ describe('Validator factory functions', () => { synchronizer, 100n, BlockNumber(5), - { rollupManaLimit: Number.MAX_SAFE_INTEGER }, + {}, new GasFees(1, 1), ); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index e15078e1bb6d..c434da61a76c 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -100,7 +100,7 @@ export function createFirstStageTxValidationsForGossipedTransactions( txsPermitted: boolean, allowedInSetup: AllowedElement[] = [], bindings?: LoggerBindings, - gasLimitOpts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }, + gasLimitOpts?: { maxTxL2Gas?: number; maxTxDAGas?: number }, ): Record { const merkleTree = worldStateSynchronizer.getCommitted(); @@ -292,24 +292,24 @@ export function createTxValidatorForAcceptingTxsOverRPC( setupAllowList, gasFees, skipFeeEnforcement, + isSimulation, timestamp, blockNumber, txsPermitted, - rollupManaLimit, - maxBlockL2Gas, - maxBlockDAGas, + maxTxL2Gas, + maxTxDAGas, }: { l1ChainId: number; rollupVersion: number; setupAllowList: AllowedElement[]; gasFees: GasFees; skipFeeEnforcement?: boolean; + isSimulation?: boolean; timestamp: UInt64; blockNumber: BlockNumber; txsPermitted: boolean; - rollupManaLimit: number; - maxBlockL2Gas?: number; - maxBlockDAGas?: number; + maxTxL2Gas?: number; + maxTxDAGas?: number; }, bindings?: LoggerBindings, ): TxValidator { @@ -339,13 +339,19 @@ export function createTxValidatorForAcceptingTxsOverRPC( new ContractInstanceTxValidator(bindings), ]; + // Declared gas-limit admission is not fee enforcement, so it runs even when fees are skipped, but it is + // skipped during simulation: gas estimation submits intentionally-inflated `forEstimation` limits (above + // the per-tx max) and the wallet clamps the real tx to the admission limit afterward, so enforcing the + // limit on the estimation tx would reject a valid estimation. The fee-balance check below stays behind + // `skipFeeEnforcement`, and GasTxValidator is constructed without the limit opts so it does not re-run + // this same check. + if (!isSimulation) { + validators.push(new GasLimitsValidator({ maxTxL2Gas, maxTxDAGas, bindings })); + } + if (!skipFeeEnforcement) { validators.push( - new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings, { - rollupManaLimit, - maxBlockL2Gas, - maxBlockDAGas, - }), + new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings), ); } @@ -431,7 +437,7 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( worldStateSynchronizer: WorldStateSynchronizer, timestamp: bigint, blockNumber: BlockNumber, - gasLimitOpts: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }, + gasLimitOpts: { maxTxL2Gas?: number; maxTxDAGas?: number }, gasFees: GasFees, bindings?: LoggerBindings, ): Promise> { diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts index d9ceb9653163..3126ddd53d78 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.test.ts @@ -11,7 +11,7 @@ import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice'; import { FunctionSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { FALLBACK_TEARDOWN_DA_GAS_LIMIT, Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { mockTx } from '@aztec/stdlib/testing'; import type { PublicStateSource } from '@aztec/stdlib/trees'; import { @@ -28,6 +28,9 @@ import { type MockProxy, mock, mockFn } from 'jest-mock-extended'; import { GasLimitsValidator, GasTxValidator, MaxFeePerGasValidator } from './gas_validator.js'; import { patchNonRevertibleFn, patchRevertibleFn } from './test_utils.js'; +const DEFAULT_GAS_LIMITS = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); +const TEARDOWN_DA_GAS = 98_304; + describe('GasTxValidator', () => { // Vars for validator. let publicStateSource: MockProxy; @@ -48,7 +51,10 @@ describe('GasTxValidator', () => { tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); tx.data.feePayer = await AztecAddress.random(); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas: gasFees.clone() }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: gasFees.clone(), + }); payer = tx.data.feePayer; expectedBalanceSlot = await computeFeePayerBalanceStorageSlot(payer); feeLimit = tx.data.constants.txContext.gasSettings.getFeeLimit().toBigInt(); @@ -118,7 +124,10 @@ describe('GasTxValidator', () => { }); assert(!privateTx.data.forPublic); privateTx.data.feePayer = payer; - privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas: gasFees.clone() }); + privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: gasFees.clone(), + }); return privateTx; }; @@ -191,7 +200,7 @@ describe('GasTxValidator', () => { tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expectInvalid(tx, TX_ERROR_GAS_LIMIT_TOO_HIGH); }); @@ -201,19 +210,19 @@ describe('GasTxValidator', () => { privateTx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expectInvalid(privateTx, TX_ERROR_GAS_LIMIT_TOO_HIGH); }); - describe('block gas limits (rollupManaLimit, maxBlockL2Gas, maxBlockDAGas)', () => { - it('rejects tx exceeding rollupManaLimit (L2)', async () => { - const rollupManaLimit = 1_000_000; - const validator = new GasLimitsValidator({ rollupManaLimit }); + describe('network admission limits (maxTxL2Gas, maxTxDAGas)', () => { + it('rejects tx exceeding maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, rollupManaLimit + 1), + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -221,29 +230,24 @@ describe('GasTxValidator', () => { }); }); - it('rejects tx exceeding maxBlockL2Gas', async () => { - const maxBlockL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxBlockL2Gas }); + it('accepts tx at exactly maxTxL2Gas', async () => { + const maxTxL2Gas = 1_000_000; + const validator = new GasLimitsValidator({ maxTxL2Gas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxBlockL2Gas + 1), + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ - result: 'invalid', - reason: [expect.stringContaining(TX_ERROR_GAS_LIMIT_TOO_HIGH)], + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); + await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); - it('uses the minimum of all L2 limits', async () => { - const rollupManaLimit = 2_000_000; - const maxBlockL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ rollupManaLimit, maxBlockL2Gas }); - // Between maxBlockL2Gas and rollupManaLimit — should be rejected (min wins) + it('clamps maxTxL2Gas to the per-tx protocol maximum', async () => { + // Passing a higher network limit cannot raise the ceiling above MAX_PROCESSABLE_L2_GAS. + const validator = new GasLimitsValidator({ maxTxL2Gas: MAX_PROCESSABLE_L2_GAS + 1_000 }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, 1_500_000), + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -251,23 +255,12 @@ describe('GasTxValidator', () => { }); }); - it('accepts tx at exactly the effective L2 limit', async () => { - const maxBlockL2Gas = 1_000_000; - const validator = new GasLimitsValidator({ maxBlockL2Gas }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxBlockL2Gas), - maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), - }); - await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); - }); - - it('falls back to MAX_PROCESSABLE_L2_GAS when no additional L2 limits are set', async () => { + it('falls back to MAX_PROCESSABLE_L2_GAS when no L2 limit is set', async () => { const validator = new GasLimitsValidator(); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -275,13 +268,13 @@ describe('GasTxValidator', () => { }); }); - it('rejects tx exceeding maxBlockDAGas', async () => { - const maxBlockDAGas = 100_000; - const validator = new GasLimitsValidator({ maxBlockDAGas }); + it('rejects tx exceeding maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxBlockDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -289,23 +282,23 @@ describe('GasTxValidator', () => { }); }); - it('accepts tx at exactly the effective DA limit', async () => { - const maxBlockDAGas = 100_000; - const validator = new GasLimitsValidator({ maxBlockDAGas }); + it('accepts tx at exactly maxTxDAGas', async () => { + const maxTxDAGas = 100_000; + const validator = new GasLimitsValidator({ maxTxDAGas }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxBlockDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), + gasLimits: new Gas(maxTxDAGas, PUBLIC_TX_L2_GAS_OVERHEAD), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); - it('caps DA at the max tx blob size when no DA block limit is set', async () => { + it('caps DA at the max tx blob size when no DA limit is set', async () => { const validator = new GasLimitsValidator(); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS + 1, PUBLIC_TX_L2_GAS_OVERHEAD), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -313,25 +306,25 @@ describe('GasTxValidator', () => { }); }); - it('accepts a tx at exactly the max tx blob size DA limit when no DA block limit is set', async () => { + it('accepts a tx at exactly the max tx blob size DA limit when no DA limit is set', async () => { const validator = new GasLimitsValidator(); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ gasLimits: new Gas(MAX_TX_DA_GAS, PUBLIC_TX_L2_GAS_OVERHEAD), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); - it('forwards limits through GasTxValidator', async () => { - const maxBlockL2Gas = 1_000_000; + it('forwards L2 limits through GasTxValidator', async () => { + const maxTxL2Gas = 1_000_000; const validator = new GasTxValidator(publicStateSource, feeJuiceAddress, gasFees, undefined, { - maxBlockL2Gas, + maxTxL2Gas, }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(MAX_TX_DA_GAS, maxBlockL2Gas + 1), + gasLimits: new Gas(MAX_TX_DA_GAS, maxTxL2Gas + 1), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -340,14 +333,14 @@ describe('GasTxValidator', () => { }); it('forwards DA limits through GasTxValidator', async () => { - const maxBlockDAGas = 100_000; + const maxTxDAGas = 100_000; const validator = new GasTxValidator(publicStateSource, feeJuiceAddress, gasFees, undefined, { - maxBlockDAGas, + maxTxDAGas, }); tx.data.constants.txContext.gasSettings = GasSettings.fallback({ - gasLimits: new Gas(maxBlockDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), + gasLimits: new Gas(maxTxDAGas + 1, PUBLIC_TX_L2_GAS_OVERHEAD), maxFeesPerGas: gasFees.clone(), - teardownGasLimits: new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, 1), + teardownGasLimits: new Gas(TEARDOWN_DA_GAS, 1), }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', @@ -373,7 +366,10 @@ describe('MaxFeePerGasValidator', () => { const gasFees = new GasFees(10, 20); const validator = new MaxFeePerGasValidator(gasFees); const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas: new GasFees(10, 20) }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: new GasFees(10, 20), + }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' }); }); @@ -381,7 +377,10 @@ describe('MaxFeePerGasValidator', () => { const gasFees = new GasFees(10, 20); const validator = new MaxFeePerGasValidator(gasFees); const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas: new GasFees(9, 20) }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: new GasFees(9, 20), + }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [expect.stringContaining(TX_ERROR_INSUFFICIENT_FEE_PER_GAS)], @@ -392,7 +391,10 @@ describe('MaxFeePerGasValidator', () => { const gasFees = new GasFees(10, 20); const validator = new MaxFeePerGasValidator(gasFees); const tx = await mockTx(1, { numberOfNonRevertiblePublicCallRequests: 2 }); - tx.data.constants.txContext.gasSettings = GasSettings.fallback({ maxFeesPerGas: new GasFees(10, 19) }); + tx.data.constants.txContext.gasSettings = GasSettings.fallback({ + gasLimits: DEFAULT_GAS_LIMITS, + maxFeesPerGas: new GasFees(10, 19), + }); await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'invalid', reason: [expect.stringContaining(TX_ERROR_INSUFFICIENT_FEE_PER_GAS)], diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts index 4cf0a56cdcab..f076bb0a481c 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/gas_validator.ts @@ -1,5 +1,4 @@ import { - MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS, PRIVATE_TX_L2_GAS_OVERHEAD, @@ -65,24 +64,21 @@ export class GasLimitsValidator implements TxValidato #log: Logger; #effectiveMaxL2Gas: number; #effectiveMaxDAGas: number; - #rollupManaLimit: number; - #maxBlockL2Gas: number; - #maxBlockDAGas: number; - constructor(opts?: { - rollupManaLimit?: number; - maxBlockL2Gas?: number; - maxBlockDAGas?: number; - bindings?: LoggerBindings; - }) { + /** + * @param maxTxL2Gas - The network admission limit on L2 gas a single tx may declare (the per-block mana + * allocation, see {@link computeNetworkTxGasLimits}). Defaults to the per-tx protocol maximum, so callers + * that pass nothing (e.g. block building) enforce only the protocol ceiling. + * @param maxTxDAGas - The network admission limit on DA gas a single tx may declare. Defaults to the + * per-tx protocol maximum {@link MAX_TX_DA_GAS}. + */ + constructor(opts?: { maxTxL2Gas?: number; maxTxDAGas?: number; bindings?: LoggerBindings }) { this.#log = createLogger('sequencer:tx_validator:tx_gas', opts?.bindings); - this.#rollupManaLimit = opts?.rollupManaLimit ?? Infinity; - this.#maxBlockL2Gas = opts?.maxBlockL2Gas ?? Infinity; - this.#maxBlockDAGas = opts?.maxBlockDAGas ?? Infinity; - this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, this.#rollupManaLimit, this.#maxBlockL2Gas); - // MAX_TX_DA_GAS bounds the limit by what a single tx can actually post to a blob; declaring more is - // meaningless and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. - this.#effectiveMaxDAGas = Math.min(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, this.#maxBlockDAGas, MAX_TX_DA_GAS); + // The passed limits are network admission limits; clamp to the per-tx protocol maxima as a hard ceiling. + // MAX_TX_DA_GAS bounds DA by what a single tx can actually post to a blob; declaring more is meaningless + // and would let a tx reserve checkpoint/block DA budget during proposal building it can't use. + this.#effectiveMaxL2Gas = Math.min(MAX_PROCESSABLE_L2_GAS, opts?.maxTxL2Gas ?? Infinity); + this.#effectiveMaxDAGas = Math.min(MAX_TX_DA_GAS, opts?.maxTxDAGas ?? Infinity); } validateTx(tx: T): Promise { @@ -114,8 +110,6 @@ export class GasLimitsValidator implements TxValidato this.#log.verbose(`Rejecting transaction due to the L2 gas limit being higher than the effective maximum`, { gasLimits, effectiveMaxL2Gas: this.#effectiveMaxL2Gas, - rollupManaLimit: this.#rollupManaLimit, - maxBlockL2Gas: this.#maxBlockL2Gas, }); return { result: 'invalid', @@ -127,7 +121,6 @@ export class GasLimitsValidator implements TxValidato this.#log.verbose(`Rejecting transaction due to the DA gas limit being higher than the effective maximum`, { gasLimits, effectiveMaxDAGas: this.#effectiveMaxDAGas, - maxBlockDAGas: this.#maxBlockDAGas, }); return { result: 'invalid', @@ -204,14 +197,14 @@ export class GasTxValidator implements TxValidator { #publicDataSource: PublicStateSource; #feeJuiceAddress: AztecAddress; #gasFees: GasFees; - #gasLimitOpts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }; + #gasLimitOpts?: { maxTxL2Gas?: number; maxTxDAGas?: number }; constructor( publicDataSource: PublicStateSource, feeJuiceAddress: AztecAddress, gasFees: GasFees, private bindings?: LoggerBindings, - opts?: { rollupManaLimit?: number; maxBlockL2Gas?: number; maxBlockDAGas?: number }, + opts?: { maxTxL2Gas?: number; maxTxDAGas?: number }, ) { this.#log = createLogger('sequencer:tx_validator:tx_gas', bindings); this.#publicDataSource = publicDataSource; diff --git a/yarn-project/p2p/src/services/gossipsub/README.md b/yarn-project/p2p/src/services/gossipsub/README.md index 5736bc70d5ef..badf193f5853 100644 --- a/yarn-project/p2p/src/services/gossipsub/README.md +++ b/yarn-project/p2p/src/services/gossipsub/README.md @@ -205,7 +205,7 @@ This ensures P3 max penalty (-34) exceeds P1 + P2 max (+33), causing mesh prunin | Topic | Expected/Slot | Decay Window | Notes | |-------|--------------|--------------|-------| | `tx` | Unpredictable | N/A | P3/P3b disabled | -| `block_proposal` | N-1 | 3 slots | N = blocks per slot (MBPS mode) | +| `block_proposal` | N-1 | 3 slots | N = blocks per slot | | `checkpoint_proposal` | 1 | 5 slots | One per slot | | `checkpoint_attestation` | C (~48) | 2 slots | C = committee size | @@ -223,15 +223,15 @@ Block proposal scoring is controlled by the `expectedBlockProposalsPerSlot` conf |-------------|----------| | `0` (current default) | Block proposal P3 scoring is **disabled** | | Positive number | Uses the provided value as expected proposals per slot | -| `undefined` | Falls back to `blocksPerSlot - 1` (MBPS mode: N-1, single block: 0) | +| `undefined` | Falls back to `blocksPerSlot - 1` (N-1 when the slot fits multiple blocks, 0 when it fits one) | **Current behavior note:** In the current implementation, if `SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT` is not set, config mapping applies `0` by default (scoring disabled). The `undefined` fallback above is currently reachable only if the value is explicitly provided as `undefined` in code. **Future intent:** Once throughput is stable, we may change env parsing/defaults so an unset env var resolves to `undefined` again (re-enabling automatic fallback to `blocksPerSlot - 1`). -**Why disabled by default?** In MBPS mode, gossipsub expects N-1 block proposals per slot. When transaction throughput is low (as expected at launch), fewer blocks are actually built, causing peers to be incorrectly penalized for under-delivering block proposals. The default of 0 disables this scoring. Set to a positive value when throughput increases and block production is consistent. +**Why disabled by default?** Gossipsub expects N-1 block proposals per slot. When transaction throughput is low (as expected at launch), fewer blocks are actually built, causing peers to be incorrectly penalized for under-delivering block proposals. The default of 0 disables this scoring. Set to a positive value when throughput increases and block production is consistent. -In MBPS mode (when enabled), N-1 block proposals are gossiped per slot (the last block is bundled with the checkpoint). In single-block mode, this is 0. +When a slot fits multiple blocks, N-1 block proposals are gossiped per slot (the last block is bundled with the checkpoint). When the slot fits exactly one block, this is 0. ### Checkpoint Proposals (checkpoint_proposal) @@ -254,7 +254,7 @@ The scoring parameters depend on: | `slotDuration` | L1RollupConstants | 72s | | `targetCommitteeSize` | L1RollupConstants | 48 | | `heartbeatInterval` | P2PConfig.gossipsubInterval | 700ms | -| `blockDurationMs` | P2PConfig.blockDurationMs | undefined (single block) | +| `blockDurationMs` | P2PConfig.blockDurationMs | 3000ms | | `expectedBlockProposalsPerSlot` | P2PConfig.expectedBlockProposalsPerSlot | 0 (disabled; current unset-env behavior) | ## Invalid Message Handling (P4) @@ -470,7 +470,22 @@ Application scores decay by 10% per minute (`decayFactor = 0.9`): - Score -100 → -35 after 10 minutes - Score -100 → -12 after 20 minutes -This allows honest peers to recover from temporary issues. +This allows honest peers to recover from temporary issues — but only up to the ban threshold. Once a peer +crosses into the **Banned** state, decay no longer applies until the ban expires (see Ban Duration below). + +### Ban Duration + +Once a peer's score drops below the ban threshold (`MIN_SCORE_BEFORE_BAN = -100`) the ban is held for a configurable +duration: + +- The score the peer held when banned is recorded in memory alongside an expiry timestamp. +- While the ban is active, `getScore` returns the **ban score** regardless of decay, so the peer stays in the + `Banned` state for the full window and cannot decay its way out early. +- When the ban expires it is removed and the live (decayed) score takes over again, letting the peer recover. + +The ban duration is controlled by `P2P_PEER_BAN_DURATION_SECONDS` (config field `peerBanDurationSeconds`), defaulting +to 24 hours. Bans are held in memory only (cleared on restart). This is independent of the gossipsub topic-score +decay (P4, P3b), which continues to decay as described above; only the application-level ban score is pinned. ## Score Calculation Examples @@ -544,15 +559,22 @@ Initial state: Total score -3003 After 4 slots (~5 min): P4 decays to 1%: -2000 → -20 - App score unchanged: -1000 + App score pinned at ban floor: -1000 Total: -1023 → Still banned, but no longer graylisted -After 10 min: - App score decays: -100 → -35 → -350 contribution - P4 further decayed: ~-5 - Total: -358 → Above gossipThreshold, starting to recover +For the rest of the ban window (default 24h): + Topic scores (P4, P3b) keep decaying toward 0 + App score stays pinned at the ban floor: -1000 contribution + Total: ~-1000 → Remains banned (cannot publish) + +After the ban expires: + The ban is lifted; the live app score (now decayed toward 0) takes over + Total: recovers, peer can participate again ``` +Unlike topic scores, the application ban score does **not** decay-recover during the ban window — that is the +point of the ban duration (see above). A banned peer is held for the full `P2P_PEER_BAN_DURATION_SECONDS`. + ## Network Outage Analysis What happens when a peer experiences a network outage and stops delivering messages? diff --git a/yarn-project/p2p/src/services/gossipsub/topic_score_params.test.ts b/yarn-project/p2p/src/services/gossipsub/topic_score_params.test.ts index bca13d4b06e1..1e03c89b6c95 100644 --- a/yarn-project/p2p/src/services/gossipsub/topic_score_params.test.ts +++ b/yarn-project/p2p/src/services/gossipsub/topic_score_params.test.ts @@ -1,3 +1,4 @@ +import { DEFAULT_BLOCK_DURATION_MS } from '@aztec/stdlib/config'; import { TopicType, createTopicString } from '@aztec/stdlib/p2p'; import { DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, @@ -24,7 +25,7 @@ import { /** * Builds a {@link ProposerTimetable} for scoring tests, mirroring how p2p builds it from config: - * operational budgets fall back to the shared stdlib defaults and `enforce` is true. + * operational budgets fall back to the shared stdlib defaults. */ function makeTimetable(opts: { slotDurationMs: number; @@ -39,30 +40,31 @@ function makeTimetable(opts: { slotDuration: opts.slotDurationMs / 1000, ethereumSlotDuration: opts.ethereumSlotDuration, }, - blockDuration: opts.blockDurationMs ? opts.blockDurationMs / 1000 : undefined, + blockDuration: (opts.blockDurationMs ?? DEFAULT_BLOCK_DURATION_MS) / 1000, minBlockDuration: DEFAULT_MIN_BLOCK_DURATION, p2pPropagationTime: opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME, checkpointProposalPrepareTime: opts.checkpointProposalPrepareTime ?? DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, checkpointProposalInitTime: DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - enforce: true, }); } describe('Topic Score Params', () => { - // Standard network parameters for testing (matching production values). The timetable defaults to - // single-block mode, so blocksPerSlot is 1 unless a test supplies a multi-block timetable. + // Standard network parameters for testing (matching production values). const standardParams = { slotDurationMs: 72000, // 72 seconds heartbeatIntervalMs: 700, // 700ms gossipsub heartbeat targetCommitteeSize: 48, - timetable: makeTimetable({ slotDurationMs: 72000, ethereumSlotDuration: 12, checkpointProposalPrepareTime: 1 }), + // 30s block duration in a 72s slot derives exactly one block per checkpoint, so these tests exercise + // single-block-mode scoring without relying on the removed "no blockDurationMs = single block" default. + timetable: makeTimetable({ + slotDurationMs: 72000, + ethereumSlotDuration: 12, + blockDurationMs: 30000, + checkpointProposalPrepareTime: 1, + }), }; describe('max blocks per checkpoint from the proposer timetable', () => { - it('returns 1 when blockDuration is undefined (single block mode)', () => { - expect(makeTimetable({ slotDurationMs: 72000, ethereumSlotDuration: 12 }).getMaxBlocksPerCheckpoint()).toBe(1); - }); - it('matches the production worked example (10 blocks)', () => { // floor((72 - 6 - 2*2 - 1) / 6) = floor(61/6) = 10 const timetable = makeTimetable({ @@ -246,7 +248,7 @@ describe('Topic Score Params', () => { it('computes shared values once', () => { const factory = new TopicScoreParamsFactory(standardParams); - expect(factory.blocksPerSlot).toBe(1); // undefined blockDuration = single block + expect(factory.blocksPerSlot).toBe(1); // 30s block duration in a 72s slot = single block expect(factory.heartbeatsPerSlot).toBeCloseTo(72000 / 700); expect(factory.invalidDecay).toBeGreaterThan(0); expect(factory.invalidDecay).toBeLessThan(1); diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts index 276debeeba3e..b688db763ad7 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -586,7 +586,7 @@ describe('LibP2PService', () => { mockEpochCache, ); - blockReceivedCallback = jest.fn().mockImplementation(() => Promise.resolve(true)); + blockReceivedCallback = jest.fn().mockImplementation(() => Promise.resolve(true)); duplicateProposalCallback = jest.fn(); service.registerBlockReceivedCallback(blockReceivedCallback as any); service.registerDuplicateProposalCallback(duplicateProposalCallback); @@ -785,6 +785,29 @@ describe('LibP2PService', () => { expect(reportMessageValidationResultSpy).toHaveBeenCalledWith('msg-1', MOCK_PEER_ID, TopicValidatorResult.Reject); }); + it('local validation failure releases the protections it created', async () => { + const header = makeBlockHeader(1, { slotNumber: targetSlot }); + const proposal = await makeBlockProposal({ signer, blockHeader: header }); + blockReceivedCallback.mockImplementationOnce(() => Promise.resolve(false)); + + await service.processBlockFromPeer(proposal.toBuffer(), 'msg-1', mockPeerId); + + expect(mockTxPool.protectTxs).toHaveBeenCalledTimes(1); + // The failed proposal releases exactly the txs it protected, keyed to its slot. + expect(mockTxPool.unprotectTxs).toHaveBeenCalledTimes(1); + expect(mockTxPool.unprotectTxs).toHaveBeenCalledWith(proposal.txHashes, targetSlot); + }); + + it('successful local validation does not release protections', async () => { + const header = makeBlockHeader(1, { slotNumber: targetSlot }); + const proposal = await makeBlockProposal({ signer, blockHeader: header }); + + await service.processBlockFromPeer(proposal.toBuffer(), 'msg-1', mockPeerId); + + expect(mockTxPool.protectTxs).toHaveBeenCalledTimes(1); + expect(mockTxPool.unprotectTxs).not.toHaveBeenCalled(); + }); + // Regression for A-1013: payloads sharing (slot, position, archive) but differing on another // signed field (e.g. inHash) used to dedup by archive only and silently drop the second one. // The pool now dedups by signed-payload hash, so the equivocation surfaces. diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index d45575f84602..9bb9ae17b8b9 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -8,7 +8,7 @@ import type { AztecAsyncKVStore } from '@aztec/kv-store'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import type { EthAddress, L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; -import { type BlockMinFeesProvider, GasFees } from '@aztec/stdlib/gas'; +import { type BlockMinFeesProvider, GasFees, getNetworkTxGasLimits } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier, PeerInfo, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { BlockProposal, @@ -24,13 +24,7 @@ import { getTopicsForConfig, metricsTopicStrToLabels, } from '@aztec/stdlib/p2p'; -import { - DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, - DEFAULT_MIN_BLOCK_DURATION, - DEFAULT_P2P_PROPAGATION_TIME, - ProposerTimetable, -} from '@aztec/stdlib/timetable'; +import { buildProposerTimetable } from '@aztec/stdlib/timetable'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { Tx, type TxValidationResult } from '@aztec/stdlib/tx'; import type { UInt64 } from '@aztec/stdlib/types'; @@ -124,29 +118,6 @@ import type { } from '../service.js'; import { P2PInstrumentation } from './instrumentation.js'; -/** - * Builds the proposer timetable used by both the gossip validators (for receive-window bounds) and - * gossipsub topic scoring (for max-blocks-per-checkpoint). Operational budgets come from p2p config, - * falling back to the shared stdlib defaults; `enforce` is true so the block-sub-slot count matches the - * proposer. `checkpointProposalInitTime` is not config-mapped, so the stdlib default is used here, the - * same way the sequencer sources it. - */ -function buildProposerTimetable( - config: P2PConfig, - l1Constants: ReturnType, -): ProposerTimetable { - return new ProposerTimetable({ - l1Constants, - blockDuration: config.blockDurationMs !== undefined ? config.blockDurationMs / 1000 : undefined, - minBlockDuration: config.minBlockDuration ?? DEFAULT_MIN_BLOCK_DURATION, - p2pPropagationTime: config.attestationPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME, - checkpointProposalPrepareTime: config.checkpointProposalPrepareTime ?? DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, - checkpointProposalInitTime: DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - checkpointProposalSyncGrace: config.checkpointProposalSyncGraceSeconds, - enforce: true, - }); -} - interface ValidationResult { name: string; isValid: TxValidationResult; @@ -465,7 +436,7 @@ export class LibP2PService extends WithTracer implements P2PService { }, connectionGater: { denyInboundConnection: (maConn: MultiaddrConnection) => { - const allowed = peerManager.isNodeAllowedToConnect(maConn.remoteAddr.nodeAddress().address); + const allowed = peerManager.isAddressAllowedToConnect(maConn.remoteAddr.nodeAddress().address); if (allowed) { return false; } @@ -476,7 +447,7 @@ export class LibP2PService extends WithTracer implements P2PService { denyInboundEncryptedConnection: (peerId: PeerId, _maConn: MultiaddrConnection) => { //NOTE: it is not necessary to check address here because this was already done by // denyInboundConnection - const allowed = peerManager.isNodeAllowedToConnect(peerId); + const allowed = peerManager.isPeerAllowedToConnect(peerId); if (allowed) { return false; } @@ -1365,6 +1336,9 @@ export class LibP2PService extends WithTracer implements P2PService { const isValid = await this.blockReceivedCallback(block, sender); if (!isValid) { this.logger.info(`Block proposal validation failed for block ${block.blockNumber}`, block.toBlockInfo()); + // Release the protections this proposal created so its txs return to pending. Only entries still + // keyed to this slot are cleared, so a tx referenced by a live proposal at another slot stays protected. + await this.mempools.txPool.unprotectTxs(block.txHashes, slot); } } @@ -1712,6 +1686,7 @@ export class LibP2PService extends WithTracer implements P2PService { ]; const blockNumber = BlockNumber(currentBlockNumber + 1); const l1Constants = await this.archiver.getL1Constants(); + const networkTxGasLimits = getNetworkTxGasLimits(this.config, l1Constants); return createFirstStageTxValidationsForGossipedTransactions( nextSlotTimestamp, @@ -1726,9 +1701,8 @@ export class LibP2PService extends WithTracer implements P2PService { allowedInSetup, this.logger.getBindings(), { - rollupManaLimit: l1Constants.rollupManaLimit, - maxBlockL2Gas: this.config.validateMaxL2BlockGas, - maxBlockDAGas: this.config.validateMaxDABlockGas, + maxTxL2Gas: networkTxGasLimits.l2Gas, + maxTxDAGas: networkTxGasLimits.daGas, }, ); } diff --git a/yarn-project/p2p/src/services/peer-manager/peer_manager.test.ts b/yarn-project/p2p/src/services/peer-manager/peer_manager.test.ts index 3bd1e140582c..8b9dadcd65a2 100644 --- a/yarn-project/p2p/src/services/peer-manager/peer_manager.test.ts +++ b/yarn-project/p2p/src/services/peer-manager/peer_manager.test.ts @@ -302,7 +302,7 @@ describe('PeerManager', () => { (pm as any).markAuthHandshakeFailed(peerId); } - expect(pm.isNodeAllowedToConnect(peerIdStr)).toBe(false); + expect(pm.isPeerAllowedToConnect(peerIdStr)).toBe(false); mockLibP2PNode.dial.mockClear(); await lastPeerCb(enr); @@ -1750,8 +1750,8 @@ describe('PeerManager', () => { // Mark auth handshake as failed once (should be below threshold) (peerManager as any).markAuthHandshakeFailed(peerId); - expect(peerManager.isNodeAllowedToConnect(peerIdStr)).toBe(true); - expect(peerManager.isNodeAllowedToConnect('192.168.1.100')).toBe(true); + expect(peerManager.isPeerAllowedToConnect(peerIdStr)).toBe(true); + expect(peerManager.isAddressAllowedToConnect('192.168.1.100')).toBe(true); }); it('should deny connection when failed attempts exceed threshold', async () => { @@ -1774,8 +1774,40 @@ describe('PeerManager', () => { (peerManager as any).markAuthHandshakeFailed(peerId); } - expect(peerManager.isNodeAllowedToConnect(peerIdStr)).toBe(false); - expect(peerManager.isNodeAllowedToConnect(ipAddress)).toBe(false); + expect(peerManager.isPeerAllowedToConnect(peerIdStr)).toBe(false); + expect(peerManager.isAddressAllowedToConnect(ipAddress)).toBe(false); + }); + + it('should deny connection from a banned peer', async () => { + const peerId = await createSecp256k1PeerId(); + const peerIdStr = peerId.toString(); + + // A peer with no penalties is allowed to connect. + expect(peerManager.isPeerAllowedToConnect(peerIdStr)).toBe(true); + + // Drive the peer's score below the ban threshold (2 × LowTolerance + 1 × HighTolerance = -102). + peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError); + peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError); + peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError); + + // The connection gater (via isPeerAllowedToConnect) now refuses the banned peer, so it cannot + // reconnect for the ban duration. + expect(peerManager.isPeerAllowedToConnect(peerIdStr)).toBe(false); + }); + + it('allows the address but denies the banned peer id', async () => { + const peerId = await createSecp256k1PeerId(); + const ipAddress = '192.168.1.123'; + + // Ban the peer (2 × LowTolerance + 1 × HighTolerance = -102). + peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError); + peerManager.penalizePeer(peerId, PeerErrorSeverity.LowToleranceError); + peerManager.penalizePeer(peerId, PeerErrorSeverity.HighToleranceError); + + // The raw-inbound gate has only the address (no peer id to match a ban), so it allows the + // connection; the encrypted-inbound gate then denies the banned peer once its id is known. + expect(peerManager.isAddressAllowedToConnect(ipAddress)).toBe(true); + expect(peerManager.isPeerAllowedToConnect(peerId.toString())).toBe(false); }); it('should increment failure counters on subsequent failures', async () => { @@ -1848,15 +1880,15 @@ describe('PeerManager', () => { } // Should be denied - expect(configuredPeerManager.isNodeAllowedToConnect(peerIdStr)).toBe(false); - expect(configuredPeerManager.isNodeAllowedToConnect(ipAddress)).toBe(false); + expect(configuredPeerManager.isPeerAllowedToConnect(peerIdStr)).toBe(false); + expect(configuredPeerManager.isAddressAllowedToConnect(ipAddress)).toBe(false); // Advance time past expiry (1 hour + 1 minute) jest.advanceTimersByTime(61 * 60 * 1000); // Should now be allowed again - expect(configuredPeerManager.isNodeAllowedToConnect(peerIdStr)).toBe(true); - expect(configuredPeerManager.isNodeAllowedToConnect(ipAddress)).toBe(true); + expect(configuredPeerManager.isPeerAllowedToConnect(peerIdStr)).toBe(true); + expect(configuredPeerManager.isAddressAllowedToConnect(ipAddress)).toBe(true); // Verify entries were cleaned up const failedHandshakes = (configuredPeerManager as any).failedAuthHandshakes; @@ -1937,14 +1969,14 @@ describe('PeerManager', () => { (peerManager as any).markAuthHandshakeFailed(peerId); } - expect(peerManager.isNodeAllowedToConnect(peerIdStr)).toBe(false); - expect(peerManager.isNodeAllowedToConnect(ipAddress)).toBe(false); + expect(peerManager.isPeerAllowedToConnect(peerIdStr)).toBe(false); + expect(peerManager.isAddressAllowedToConnect(ipAddress)).toBe(false); // Advance time past expiry (1 hour + 1 minute) jest.advanceTimersByTime(61 * 60 * 1000); - // Trigger heartbeat — should proactively clean up stale entries without needing - // isNodeAllowedToConnect to be called first for each peer + // Trigger heartbeat — should proactively clean up stale entries without needing a + // connection-allowed check (isPeerAllowedToConnect / isAddressAllowedToConnect) first await peerManager.heartbeat(); const failedHandshakes = (peerManager as any).failedAuthHandshakes; @@ -2049,8 +2081,8 @@ describe('PeerManager', () => { } // Should now be blocked - expect(peerManager.isNodeAllowedToConnect(peerId)).toBe(false); - expect(peerManager.isNodeAllowedToConnect(ipAddress)).toBe(false); + expect(peerManager.isPeerAllowedToConnect(peerId)).toBe(false); + expect(peerManager.isAddressAllowedToConnect(ipAddress)).toBe(false); // Peer should not be authenticated expect(peerManager.isAuthenticatedPeer(peerId)).toBe(false); 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 68ddf5eee868..a7ba6f37e0ad 100644 --- a/yarn-project/p2p/src/services/peer-manager/peer_manager.ts +++ b/yarn-project/p2p/src/services/peer-manager/peer_manager.ts @@ -164,6 +164,7 @@ export class PeerManager implements PeerManagerInterface { public async heartbeat() { this.heartbeatCounter++; this.peerScoring.decayAllScores(); + this.peerScoring.pruneExpiredBans(); this.cleanupExpiredTimeouts(); await this.setupDirectPeersIfValidator(); @@ -494,15 +495,38 @@ export class PeerManager implements PeerManagerInterface { * * @returns: True if node is allowed to connect, otherwise false * */ - public isNodeAllowedToConnect(id: string | PeerId): boolean { - const entry = this.failedAuthHandshakes.get(id.toString()); + /** + * Whether a peer is allowed to connect, given its peer id. Rejects peers serving an active ban and + * peers that have exceeded the failed auth-handshake limit. Use this once the peer id is known — + * i.e. for the encrypted-inbound gater and when dialing. + */ + public isPeerAllowedToConnect(peerId: string | PeerId): boolean { + const id = peerId.toString(); + if (this.peerScoring.getScoreState(id) === PeerScoreState.Banned) { + return false; + } + return this.isWithinFailedAuthLimit(id); + } + + /** + * Whether a connection from an address is allowed. Bans are keyed by peer id, which isn't known at + * the raw-inbound layer, so only the failed auth-handshake limit (also tracked per address) is + * enforced here; the ban is applied once the peer id is known via {@link isPeerAllowedToConnect}. + */ + public isAddressAllowedToConnect(address: string): boolean { + return this.isWithinFailedAuthLimit(address); + } + + /** Whether the failed auth-handshake count for a peer id or address is below the configured limit. */ + private isWithinFailedAuthLimit(key: string): boolean { + const entry = this.failedAuthHandshakes.get(key); if (!entry) { return true; } // In case entry is too old, remove it and allow connection if (this.dateProvider.now() - entry.lastFailureTimestamp > FAILED_AUTH_HANDSHAKE_EXPIRY_MS) { - this.failedAuthHandshakes.delete(id.toString()); + this.failedAuthHandshakes.delete(key); return true; } @@ -729,9 +753,9 @@ export class PeerManager implements PeerManagerInterface { return; } - // Don't dial peers that have exceeded the auth failure threshold - if (!this.isNodeAllowedToConnect(peerId)) { - this.logger.trace(`Skipping peer ${peerId} due to failed auth handshake attempts`); + // Don't dial banned peers or those that have exceeded the auth failure threshold + if (!this.isPeerAllowedToConnect(peerId)) { + this.logger.trace(`Skipping peer ${peerId} due to ban or failed auth handshake attempts`); return; } diff --git a/yarn-project/p2p/src/services/peer-manager/peer_scoring.test.ts b/yarn-project/p2p/src/services/peer-manager/peer_scoring.test.ts index ecdd049218ad..d7f5d5076bbe 100644 --- a/yarn-project/p2p/src/services/peer-manager/peer_scoring.test.ts +++ b/yarn-project/p2p/src/services/peer-manager/peer_scoring.test.ts @@ -1,6 +1,8 @@ import { ManualDateProvider } from '@aztec/foundation/timer'; import { PeerErrorSeverity } from '@aztec/stdlib/p2p'; +import { createSecp256k1PeerId } from '@libp2p/peer-id-factory'; + import { getP2PDefaultConfig } from '../../config.js'; import { PeerScoreState, PeerScoring } from './peer_scoring.js'; @@ -109,21 +111,58 @@ describe('PeerScoring', () => { const testPeerId = 'testPeerState'; // Test Healthy state (default) + expect(peerScoring.getScore(testPeerId)).toBe(0); expect(peerScoring.getScoreState(testPeerId)).toBe(PeerScoreState.Healthy); // Test Disconnect state (score between -100 and -50) peerScoring.updateScore(testPeerId, -60); + expect(peerScoring.getScore(testPeerId)).toBe(-60); expect(peerScoring.getScoreState(testPeerId)).toBe(PeerScoreState.Disconnect); // Test Banned state (score below -100) peerScoring.updateScore(testPeerId, -50); // Total now -110 + expect(peerScoring.getScore(testPeerId)).toBe(-110); + expect(peerScoring.getScoreState(testPeerId)).toBe(PeerScoreState.Banned); + + // Improving the score does not lift the ban: getScore returns the ban floor (-110), not the + // recovered live score, for the full ban window. + peerScoring.updateScore(testPeerId, 120); // Live score now +10, but the ban floor still applies + expect(peerScoring.getScore(testPeerId)).toBe(-110); expect(peerScoring.getScoreState(testPeerId)).toBe(PeerScoreState.Banned); - // Test return to Healthy state - peerScoring.updateScore(testPeerId, 120); // Total now +10 + // Once the ban expires, the live (improved) score takes over and the peer is Healthy again. + dateProvider.advanceTimeMs(24 * 60 * 60 * 1000 + 1); + expect(peerScoring.getScore(testPeerId)).toBe(10); expect(peerScoring.getScoreState(testPeerId)).toBe(PeerScoreState.Healthy); }); + test('honours peerBanDurationSeconds for the ban window', () => { + const banDurationSeconds = 60; + const localDateProvider = new ManualDateProvider(); + const scoring = new PeerScoring( + { ...getP2PDefaultConfig(), peerPenaltyValues: [2, 10, 50], peerBanDurationSeconds: banDurationSeconds }, + undefined, + localDateProvider, + ); + const bannedPeerId = 'bannedPeer'; + + scoring.updateScore(bannedPeerId, -150); + expect(scoring.getScore(bannedPeerId)).toBe(-150); + expect(scoring.getScoreState(bannedPeerId)).toBe(PeerScoreState.Banned); + // Recover the live score so only the ban floor keeps it banned. + scoring.updateScore(bannedPeerId, 300); // live score now +150 + + // Still banned just before the configured window elapses: getScore returns the ban floor. + localDateProvider.advanceTimeMs(banDurationSeconds * 1000 - 1); + expect(scoring.getScore(bannedPeerId)).toBe(-150); + expect(scoring.getScoreState(bannedPeerId)).toBe(PeerScoreState.Banned); + + // Unbanned once it elapses: the recovered live score takes over. + localDateProvider.advanceTimeMs(2); + expect(scoring.getScore(bannedPeerId)).toBe(150); + expect(scoring.getScoreState(bannedPeerId)).toBe(PeerScoreState.Healthy); + }); + test('should handle score state transitions with decay', () => { const testPeerId = 'testPeerStateDecay'; @@ -166,4 +205,74 @@ describe('PeerScoring', () => { const stats = peerScoring.getStats(); expect(stats.healthyCount).toBe(1); }); + + test('re-bans a peer whose previous ban has expired', () => { + const reBanPeerId = 'reBanPeer'; + const DAY_MS = 24 * 60 * 60 * 1000; + + // Initial ban. + peerScoring.updateScore(reBanPeerId, -150); + expect(peerScoring.getScoreState(reBanPeerId)).toBe(PeerScoreState.Banned); + + // Let the ban expire without anyone reading the peer's score, so the stale record lingers in the + // ban map (expired bans are only pruned lazily on read). + dateProvider.advanceTimeMs(DAY_MS + 1); + + // A fresh offence after expiry must start a new ban window despite the stale record. + peerScoring.updateScore(reBanPeerId, -150); + // Recover the live score above the ban threshold so only a fresh ban floor can keep it banned. + peerScoring.updateScore(reBanPeerId, 200); + + expect(peerScoring.getScore(reBanPeerId)).toBe(-150); + expect(peerScoring.getScoreState(reBanPeerId)).toBe(PeerScoreState.Banned); + }); + + test('pruneExpiredBans removes expired bans but keeps active ones', () => { + const expiredPeer = 'expiredBanPeer'; + const activePeer = 'activeBanPeer'; + + // Ban the first peer at t0 (expires at t0 + 24h). + peerScoring.updateScore(expiredPeer, -150); + + // 23h later, ban the second peer (expires at t0 + 47h). + dateProvider.advanceTimeMs(23 * 60 * 60 * 1000); + peerScoring.updateScore(activePeer, -150); + + // Advance to t0 + 25h: the first ban has expired, the second is still active. Neither has been + // read, so both records still linger in the map. + dateProvider.advanceTimeMs(2 * 60 * 60 * 1000); + + peerScoring.pruneExpiredBans(); + + // The sweep dropped the expired ban from the map proactively, without a getScore read, and kept + // the active one. + const bannedPeers = (peerScoring as any).bannedPeers as Map; + expect(bannedPeers.has(expiredPeer)).toBe(false); + expect(bannedPeers.has(activePeer)).toBe(true); + }); + + // Regression test for the original advisory (GHSA-h4vv-85x5-6hmh): decayAllScores used to delete a + // banned peer's decayed score entry, after which getScore returned 0 and the peer was silently + // restored to Healthy — an effective ~66-minute ban. The ban must keep it Banned. + test('does not silently restore a banned peer to Healthy after decay (GHSA-h4vv-85x5-6hmh)', async () => { + const peer = await createSecp256k1PeerId(); + + // Ban the peer: 3 x LowToleranceError (50 each) = -150, below MIN_SCORE_BEFORE_BAN (-100). + peerScoring.penalizePeer(peer, PeerErrorSeverity.LowToleranceError); + peerScoring.penalizePeer(peer, PeerErrorSeverity.LowToleranceError); + peerScoring.penalizePeer(peer, PeerErrorSeverity.LowToleranceError); + expect(peerScoring.getScore(peer.toString())).toBe(-150); + expect(peerScoring.getScoreState(peer.toString())).toBe(PeerScoreState.Banned); + + // Stay idle long enough that decay drives the live score below SCORE_CLEANUP_THRESHOLD, so + // decayAllScores removes the entry (the exact mechanism the advisory exploited) — but still + // within the ban window. + dateProvider.advanceTimeMs(2 * 60 * 60 * 1000); // 2 hours + peerScoring.decayAllScores(); + + // Previously the decayed live score would have been cleaned up and getScore would have read back + // 0 (Healthy). The persisted ban score (-150) is returned instead, keeping the peer Banned. + expect(peerScoring.getScore(peer.toString())).toBe(-150); + expect(peerScoring.getScoreState(peer.toString())).toBe(PeerScoreState.Banned); + }); }); diff --git a/yarn-project/p2p/src/services/peer-manager/peer_scoring.ts b/yarn-project/p2p/src/services/peer-manager/peer_scoring.ts index 1fa97ea4da8a..cc8416ac8cf5 100644 --- a/yarn-project/p2p/src/services/peer-manager/peer_scoring.ts +++ b/yarn-project/p2p/src/services/peer-manager/peer_scoring.ts @@ -57,6 +57,9 @@ const MIN_SCORE_BEFORE_BAN = -100; const MIN_SCORE_BEFORE_DISCONNECT = -50; const SCORE_CLEANUP_THRESHOLD = 0.1; +/** An active ban: the score the peer held when banned, and the timestamp at which the ban lifts. */ +type BanRecord = { score: number; expiry: number }; + export class PeerScoring { private logger = createLogger('p2p:peer-scoring'); private scores: Map = new Map(); @@ -67,11 +70,21 @@ export class PeerScoring { private peerStateCounter: UpDownCounter; + /** Active bans, keyed by peer id. Held in memory only, so they are cleared on restart. */ + private bannedPeers: Map = new Map(); + /** + * How long a peer remains banned once its score crosses MIN_SCORE_BEFORE_BAN. While banned, the + * peer's ban score is returned by getScore regardless of decay, so it cannot recover its way out + * of the ban early. After the ban expires the live (decayed) score takes over again. + */ + private readonly banDurationMs: number; + constructor( config: P2PConfig, telemetry: TelemetryClient = getTelemetryClient(), private readonly dateProvider: DateProvider = new DateProvider(), ) { + this.banDurationMs = config.peerBanDurationSeconds * 1000; const orderedValues = config.peerPenaltyValues?.sort((a, b) => a - b); this.peerPenalties = { [PeerErrorSeverity.HighToleranceError]: @@ -113,9 +126,31 @@ export class PeerScoring { this.scores.set(peerId, currentScore); this.lastUpdateTime.set(peerId, currentTime); + + this.maybeBanPeer(peerId, currentScore); + return currentScore; } + /** + * Records a ban for a peer whose score has crossed the ban threshold, with an expiry banDurationMs + * in the future. No-op if the score is above the threshold or the peer is already serving an active + * ban (an existing ban is not extended; the original window stands). A previously expired ban does + * not block a fresh one — getActiveBanScore prunes it first. + */ + private maybeBanPeer(peerId: string, score: number): void { + if (score >= MIN_SCORE_BEFORE_BAN || this.getActiveBanScore(peerId) !== undefined) { + return; + } + const record: BanRecord = { score, expiry: this.dateProvider.now() + this.banDurationMs }; + this.bannedPeers.set(peerId, record); + this.logger.verbose(`Banning peer ${peerId} until ${new Date(record.expiry).toISOString()}`, { + peerId, + score, + expiry: record.expiry, + }); + } + decayAllScores(): void { const currentTime = this.dateProvider.now(); for (const [peerId, lastUpdate] of this.lastUpdateTime.entries()) { @@ -135,10 +170,24 @@ export class PeerScoring { } } + /** + * Removes bans whose window has elapsed. Expired bans are otherwise only pruned lazily when their + * peer's score is next queried, so a banned peer that disconnects and is never queried again would + * linger in the map. Called periodically (per heartbeat) to bound the ban map's size. + */ + pruneExpiredBans(): void { + const now = this.dateProvider.now(); + const expired = [...this.bannedPeers.entries()].filter(([, ban]) => ban.expiry <= now).map(([peerId]) => peerId); + for (const peerId of expired) { + this.bannedPeers.delete(peerId); + } + } + /** Resets all peer scores. Useful for benchmarks to prevent cross-case contamination. */ resetAllScores(): void { this.scores.clear(); this.lastUpdateTime.clear(); + this.bannedPeers.clear(); } removePeer(peerId: string): void { @@ -146,12 +195,33 @@ export class PeerScoring { this.lastUpdateTime.delete(peerId); } + /** + * The single source of truth for whether a peer is banned. Returns the ban score while the ban is + * active, or undefined if the peer is not banned. A ban that has expired is lazily lifted before + * returning undefined, so callers never see a stale ban. + */ + private getActiveBanScore(peerId: string): number | undefined { + const ban = this.bannedPeers.get(peerId); + if (ban === undefined) { + return undefined; + } + if (ban.expiry > this.dateProvider.now()) { + return ban.score; + } + // Ban expired: lift it so the peer can recover. + this.bannedPeers.delete(peerId); + return undefined; + } + getScore(peerId: string): number { - return this.scores.get(peerId) || 0; + // While a ban is active its ban score is returned regardless of how the live score has decayed, + // so the peer stays banned for the full duration. + return this.getActiveBanScore(peerId) ?? this.scores.get(peerId) ?? 0; } public getScoreState(peerId: string): PeerScoreState { - // TODO(#11329): permanently store banned peers? + // A banned peer stays banned for the full ban duration regardless of score decay (see getScore / + // maybeBanPeer), rather than silently recovering once its decayed score is cleaned up. const score = this.getScore(peerId); if (score < MIN_SCORE_BEFORE_BAN) { return PeerScoreState.Banned; @@ -165,7 +235,9 @@ export class PeerScoring { getStats(): { medianScore: number; healthyCount: number; disconnectCount: number; bannedCount: number } { const stateCounts = { healthy: 0, disconnect: 0, banned: 0 }; - for (const peerId of this.scores.keys()) { + // Include banned peers whose live score may have been decayed away but whose ban is still active. + const peerIds = new Set([...this.scores.keys(), ...this.bannedPeers.keys()]); + for (const peerId of peerIds) { const state = this.getScoreState(peerId); switch (state) { case PeerScoreState.Healthy: diff --git a/yarn-project/p2p/src/test-helpers/testbench-utils.ts b/yarn-project/p2p/src/test-helpers/testbench-utils.ts index a9671f1b78e9..26fb73ff3b89 100644 --- a/yarn-project/p2p/src/test-helpers/testbench-utils.ts +++ b/yarn-project/p2p/src/test-helpers/testbench-utils.ts @@ -119,6 +119,10 @@ export class InMemoryTxPool extends EventEmitter implements TxPoolV2 { return Promise.resolve(); } + unprotectTxs(_txHashes: TxHash[], _slotNumber: SlotNumber): Promise { + return Promise.resolve(); + } + handlePrunedBlocks(_latestBlock: L2BlockId, _options?: { deleteAllTxs?: boolean }): Promise { return Promise.resolve(); } diff --git a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts index 274c86272f13..4e14be3b8352 100644 --- a/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts +++ b/yarn-project/p2p/src/testbench/p2p_client_testbench_worker.ts @@ -17,6 +17,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { EmptyL1RollupConstants, type L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; import type { ClientProtocolCircuitVerifier, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; @@ -84,6 +85,25 @@ export interface BenchReadyMessage { } const txCache = new Map(); +const BENCH_L1_CONSTANTS: L1RollupConstants = { + ...EmptyL1RollupConstants, + slotDuration: 12, + ethereumSlotDuration: 4, +}; + +class TestbenchL2BlockSource extends MockL2BlockSource { + public override getL1Constants(): Promise { + return Promise.resolve(BENCH_L1_CONSTANTS); + } +} + +function createBenchmarkEpochCache(): EpochCacheInterface { + return { + ...createMockEpochCache(), + getL1Constants: () => BENCH_L1_CONSTANTS, + }; +} + class TestLibP2PService extends LibP2PService { private disableTxValidation: boolean; private gossipMessageCount = 0; @@ -370,9 +390,9 @@ process.on('message', async msg => { workerConfig = config; workerTxPool = new InMemoryTxPool(); workerAttestationPool = new InMemoryAttestationPool(); - const epochCache = createMockEpochCache(); + const epochCache = createBenchmarkEpochCache(); const worldState = createMockWorldStateSynchronizer(); - const l2BlockSource = new MockL2BlockSource(); + const l2BlockSource = new TestbenchL2BlockSource(); const proofVerifier = new AlwaysTrueCircuitVerifier(); kvStore = await openTmpStore(`test-${clientIndex}`, true, BENCHMARK_CONSTANTS.KV_STORE_MAP_SIZE_KB); diff --git a/yarn-project/prover-client/package.json b/yarn-project/prover-client/package.json index d55946bd264a..597f3074aa6f 100644 --- a/yarn-project/prover-client/package.json +++ b/yarn-project/prover-client/package.json @@ -9,7 +9,8 @@ "./orchestrator": "./dest/orchestrator/index.js", "./helpers": "./dest/orchestrator/block-building-helpers.js", "./light": "./dest/light/index.js", - "./config": "./dest/config.js" + "./config": "./dest/config.js", + "./test": "./dest/test/index.js" }, "typedocOptions": { "entryPoints": [ diff --git a/yarn-project/prover-client/src/test/epoch_settlement.ts b/yarn-project/prover-client/src/test/epoch_settlement.ts new file mode 100644 index 000000000000..ce600874dfb0 --- /dev/null +++ b/yarn-project/prover-client/src/test/epoch_settlement.ts @@ -0,0 +1,82 @@ +import type { RollupCheatCodes } from '@aztec/ethereum/test'; +import type { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { Logger } from '@aztec/foundation/log'; +import type { L2BlockSource } from '@aztec/stdlib/block'; +import { computeEpochOutHash } from '@aztec/stdlib/messaging'; + +/** Arguments for {@link settleEpochOutbox}. */ +export type SettleEpochOutboxArgs = { + /** Cheat codes used to write the computed out hash directly into the L1 Outbox storage. */ + rollupCheatCodes: RollupCheatCodes; + /** Source of checkpointed L2 blocks for the epoch being settled. */ + l2BlockSource: L2BlockSource; + /** Epoch to settle. */ + epoch: EpochNumber; + /** + * When set, only checkpoints up to and including this number are covered. Used to settle a partial + * epoch (the AZIP-14 Outbox keeps one root per `numCheckpointsInEpoch`, so a prefix of an epoch can + * be settled and consumed before the epoch completes). When omitted, every checkpointed block in the + * epoch is covered. + */ + maxCheckpoint?: CheckpointNumber; + log: Logger; +}; + +/** + * Computes the epoch out hash over the checkpointed blocks of an epoch (optionally only up to + * `maxCheckpoint`) and writes it into the L1 Outbox via cheat codes. This is the synthetic, + * no-prover equivalent of an epoch root proof landing on L1: it makes the L2-to-L1 messages in the + * covered checkpoints consumable. It does NOT advance the rollup's proven tip — callers + * `markAsProven` separately so a single prove call can settle multiple epochs before marking proven. + * + * Used by the test-only proving drivers: the `AutomineSequencer`'s auto-settle loop and the standalone + * `EpochTestSettler`. Lives here (rather than in either consumer) so both share one implementation. + * + * @returns The last checkpoint number covered by the settled range, or `undefined` if the epoch has + * no checkpointed blocks (within the `maxCheckpoint` bound). + */ +export async function settleEpochOutbox({ + rollupCheatCodes, + l2BlockSource, + epoch, + maxCheckpoint, + log, +}: SettleEpochOutboxArgs): Promise { + let blocks = await l2BlockSource.getBlocks({ epoch, onlyCheckpointed: true }); + if (maxCheckpoint !== undefined) { + blocks = blocks.filter(block => block.checkpointNumber <= maxCheckpoint); + } + if (blocks.length === 0) { + return undefined; + } + + log.info( + `Settling epoch ${epoch}${maxCheckpoint !== undefined ? ` up to checkpoint ${maxCheckpoint}` : ''} with blocks ${blocks[0]?.header.getBlockNumber()} to ${blocks.at(-1)?.header.getBlockNumber()}`, + { epoch, maxCheckpoint, blocks: blocks.map(block => block.toBlockInfo()) }, + ); + + const messagesInEpoch: Fr[][][][] = []; + // Undefined (not SlotNumber.ZERO) so a first checkpointed block at slot 0 still opens checkpoint 0. + let previousSlotNumber: SlotNumber | undefined; + let checkpointIndex = -1; + + for (const block of blocks) { + const slotNumber = block.header.globalVariables.slotNumber; + if (slotNumber !== previousSlotNumber) { + checkpointIndex++; + messagesInEpoch[checkpointIndex] = []; + previousSlotNumber = slotNumber; + } + messagesInEpoch[checkpointIndex].push(block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)); + } + + const outHash = computeEpochOutHash(messagesInEpoch); + if (!outHash.isZero()) { + await rollupCheatCodes.insertOutbox(epoch, messagesInEpoch.length, outHash.toBigInt()); + } else { + log.info(`No L2 to L1 messages in epoch ${epoch}`, { epoch }); + } + + return blocks.at(-1)?.checkpointNumber; +} diff --git a/yarn-project/prover-client/src/test/index.ts b/yarn-project/prover-client/src/test/index.ts new file mode 100644 index 000000000000..e3fa99589e4c --- /dev/null +++ b/yarn-project/prover-client/src/test/index.ts @@ -0,0 +1 @@ +export { settleEpochOutbox, type SettleEpochOutboxArgs } from './epoch_settlement.js'; diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index d3abd72b5cee..b9e23f2846c0 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -81,6 +81,12 @@ describe('ProverNode', () => { // ---------------- event dispatch ---------------- + /** Builds an L2TipId (block + checkpoint id) for block/checkpoint number `n`. */ + const makeTipId = (n: number) => ({ + block: { number: BlockNumber(n), hash: `0x0${n}` }, + checkpoint: { number: CheckpointNumber(n), hash: `0x0${n}` }, + }); + it('dispatches chain-checkpointed to handleCheckpointEvent', async () => { setupNotFullyProven(); const checkpoint = makeCheckpoint(1, 1, 1); @@ -100,8 +106,9 @@ describe('ProverNode', () => { // No registered checkpoints — nothing to prune. await proverNode.handleBlockStreamEvent({ type: 'chain-pruned', - checkpoint: { number: CheckpointNumber(0), hash: '0x00' }, block: { number: BlockNumber(0), hash: '0x00' }, + checkpointed: makeTipId(0), + proven: makeTipId(0), }); expect(sessionManager.onPrune).not.toHaveBeenCalled(); @@ -115,8 +122,9 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-pruned', - checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, block: { number: BlockNumber(1), hash: '0x01' }, + checkpointed: makeTipId(1), + proven: makeTipId(1), }); expect(sessionManager.onPrune).toHaveBeenCalledWith([EpochNumber(2)]); }); @@ -125,6 +133,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-proven', block: { number: BlockNumber(7), hash: '0x07' }, + checkpoint: { number: CheckpointNumber(7), hash: '0x07' }, }); expect(publishingService.onChainProven).toHaveBeenCalledWith(BlockNumber(7)); }); @@ -149,6 +158,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(cache.get(txHash)).toBeUndefined(); @@ -164,6 +174,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(reapSpy.mock.calls.length).toBe(3); reapSpy.mockClear(); @@ -172,6 +183,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(reapSpy).not.toHaveBeenCalled(); }); @@ -183,6 +195,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(reapSpy).not.toHaveBeenCalled(); }); @@ -318,6 +331,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(reapSpy).not.toHaveBeenCalled(); @@ -338,6 +352,7 @@ describe('ProverNode', () => { await proverNode.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(1), hash: '0x01' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x01' }, }); expect(reapSpy.mock.calls.map(([e]) => Number(e))).toEqual([0, 1, 2]); @@ -372,8 +387,9 @@ describe('ProverNode', () => { sessionManager.onPrune.mockClear(); await proverNode.handleBlockStreamEvent({ type: 'chain-pruned', - checkpoint: { number: CheckpointNumber(0), hash: '0x00' }, block: { number: BlockNumber(0), hash: '0x00' }, + checkpointed: makeTipId(0), + proven: makeTipId(0), }); expect(sessionManager.onPrune).toHaveBeenCalledTimes(1); expect(sessionManager.onPrune).toHaveBeenCalledWith([EpochNumber(3)]); @@ -594,6 +610,7 @@ describe('ProverNode', () => { header: { slotNumber: SlotNumber(slot) }, archive: { root: archiveRoot }, blocks: [{ number: blockNumber, header: { hash: () => Promise.resolve('0x01') } }], + hash: () => new Fr(checkpointNumber), } as unknown as Checkpoint; } diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 22c28e8c5bd4..9e5b7fbd7508 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -222,7 +222,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra await this.handleCheckpointEvent(event.checkpoint); break; case 'chain-pruned': - await this.handlePruneEvent(event.checkpoint); + await this.handlePruneEvent(event.checkpointed.checkpoint); break; case 'chain-proven': this.publishingService?.onChainProven(BlockNumber(event.block.number)); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 057659691913..bbd6d2ae9729 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -136,7 +136,14 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: makeL2BlockId(reorgBlock.number, reorgResponse.hash.toString()), - checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + checkpointed: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, + proven: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, }); // The anchor block should be updated to the reorg block header. @@ -230,7 +237,14 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: block3, - checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + checkpointed: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, + proven: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, }); // Rows at blocks 4 and 5 must be gone. @@ -265,6 +279,7 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-finalized', block: block9, + checkpoint: makeL2CheckpointId(CheckpointNumber(1), Fr.random().toString()), }); // Finalization is a no-op for storage under delete-on-prune, every row at and below the tip survives. @@ -304,7 +319,14 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: block1, - checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + checkpointed: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, + proven: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, }); // Blocks 2 and 3 deleted. @@ -408,6 +430,7 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-proven', block: { number: BlockNumber(5), hash: '0x789' }, + checkpoint: { number: CheckpointNumber(1), hash: '0x789c' }, }); const obtainedHeader = await anchorBlockStore.getBlockHeader(); @@ -428,6 +451,7 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: BlockNumber(10), hash: '0xabc' }, + checkpoint: { number: CheckpointNumber(2), hash: '0xabcc' }, }); const obtainedHeader = await anchorBlockStore.getBlockHeader(); @@ -445,7 +469,14 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: BlockNumber(3), hash: '0x3' }, - checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + checkpointed: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, + proven: { + block: makeL2BlockId(BlockNumber.ZERO, GENESIS_BLOCK_HEADER_HASH.toString()), + checkpoint: makeL2CheckpointId(CheckpointNumber.ZERO, GENESIS_CHECKPOINT_HEADER_HASH.toString()), + }, }); // Anchor should be unchanged diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts index fb9beaa9850f..e35ada6facfb 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts @@ -81,7 +81,71 @@ export { type TypeMapping, } from './oracle_type_mappings.js'; -export const ORACLE_REGISTRY = { +type OracleRegistryName = + | 'aztec_misc_assertCompatibleOracleVersion' + | 'aztec_misc_getRandomField' + | 'aztec_misc_log' + | 'aztec_utl_getUtilityContext' + | 'aztec_utl_getKeyValidationRequest' + | 'aztec_utl_getContractInstance' + | 'aztec_utl_getNoteHashMembershipWitness' + | 'aztec_utl_getBlockHashMembershipWitness' + | 'aztec_utl_getNullifierMembershipWitness' + | 'aztec_utl_getLowNullifierMembershipWitness' + | 'aztec_utl_getPublicDataWitness' + | 'aztec_utl_getBlockHeader' + | 'aztec_utl_getAuthWitness' + | 'aztec_utl_getPublicKeysAndPartialAddress' + | 'aztec_utl_doesNullifierExist' + | 'aztec_utl_getL1ToL2MembershipWitness' + | 'aztec_utl_getFromPublicStorage' + | 'aztec_utl_getNotes' + | 'aztec_utl_getPendingTaggedLogs' + | 'aztec_utl_validateAndStoreEnqueuedNotesAndEvents' + | 'aztec_utl_getLogsByTag' + | 'aztec_utl_getMessageContextsByTxHash' + | 'aztec_utl_getTxEffect' + | 'aztec_utl_setCapsule' + | 'aztec_utl_getCapsule' + | 'aztec_utl_deleteCapsule' + | 'aztec_utl_copyCapsule' + | 'aztec_utl_decryptAes128' + | 'aztec_utl_getSharedSecrets' + | 'aztec_utl_setContractSyncCacheInvalid' + | 'aztec_utl_emitOffchainEffect' + | 'aztec_utl_callUtilityFunction' + | 'aztec_utl_pushEphemeral' + | 'aztec_utl_popEphemeral' + | 'aztec_utl_getEphemeral' + | 'aztec_utl_setEphemeral' + | 'aztec_utl_getEphemeralLen' + | 'aztec_utl_removeEphemeral' + | 'aztec_utl_clearEphemeral' + | 'aztec_utl_pushTransient' + | 'aztec_utl_popTransient' + | 'aztec_utl_getTransient' + | 'aztec_utl_setTransient' + | 'aztec_utl_getTransientLen' + | 'aztec_utl_removeTransient' + | 'aztec_utl_clearTransient' + | 'aztec_prv_setHashPreimage' + | 'aztec_prv_getHashPreimage' + | 'aztec_prv_notifyCreatedNote' + | 'aztec_prv_notifyNullifiedNote' + | 'aztec_prv_notifyCreatedNullifier' + | 'aztec_prv_isNullifierPending' + | 'aztec_prv_notifyCreatedContractClassLog' + | 'aztec_prv_callPrivateFunction' + | 'aztec_prv_assertValidPublicCalldata' + | 'aztec_prv_notifyRevertiblePhaseStart' + | 'aztec_prv_isExecutionInRevertiblePhase' + | 'aztec_prv_getAppTaggingSecret' + | 'aztec_prv_getNextTaggingIndex' + | 'aztec_prv_getSenderForTags'; + +type OracleRegistry = Record; + +export const ORACLE_REGISTRY: OracleRegistry = { aztec_misc_assertCompatibleOracleVersion: makeEntry({ params: [ { name: 'major', type: U32 }, @@ -502,7 +566,7 @@ export const ORACLE_REGISTRY = { }), aztec_prv_getSenderForTags: makeEntry({ returnType: OPTION(AZTEC_ADDRESS) }), -} satisfies Record; +}; // ─── Registry Infrastructure ───────────────────────────────────────────────── diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index bc6e55939d11..c2f27d0c0e75 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -1,3 +1,4 @@ +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; import { KeyStore } from '@aztec/key-store'; import { OracleVersionCheckContractArtifact } from '@aztec/noir-test-contracts.js/OracleVersionCheck'; @@ -6,7 +7,7 @@ import { FunctionCall, FunctionSelector, FunctionType, encodeArguments } from '@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2TipsProvider } from '@aztec/stdlib/block'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { BlockHeader, HashedValues, TxContext, TxExecutionRequest } from '@aztec/stdlib/tx'; @@ -137,7 +138,10 @@ describe('Oracle Version Check test suite', () => { txContext: TxContext.from({ chainId: new Fr(10), version: new Fr(20), - gasSettings: GasSettings.fallback({ maxFeesPerGas: new GasFees(10, 10) }), + gasSettings: GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS), + maxFeesPerGas: new GasFees(10, 10), + }), }), argsOfCalls: [hashedArguments], authWitnesses: [], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index f0c80c7eef07..8b8858ec4522 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -1,4 +1,4 @@ -import { DomainSeparator } from '@aztec/constants'; +import { DomainSeparator, MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; import { asyncMap } from '@aztec/foundation/async-map'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; @@ -33,7 +33,7 @@ import { getContractClassFromArtifact, getContractInstanceFromInstantiationParams, } from '@aztec/stdlib/contract'; -import { GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { computeNoteHashNonce, computeSecretHash, computeUniqueNoteHash, siloNoteHash } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; @@ -146,7 +146,10 @@ describe('Private Execution test suite', () => { const txContextFields: FieldsOf = { chainId: new Fr(10), version: new Fr(20), - gasSettings: GasSettings.fallback({ maxFeesPerGas: new GasFees(10, 10) }), + gasSettings: GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS), + maxFeesPerGas: new GasFees(10, 10), + }), }; let contracts: { [address: string]: ContractArtifact }; diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts index 4825288921f4..b5705dad2af8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts @@ -1,3 +1,4 @@ +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { KeyStore } from '@aztec/key-store'; import { WASMSimulator } from '@aztec/simulator/client'; @@ -5,7 +6,7 @@ import { FunctionSelector } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2TipsProvider } from '@aztec/stdlib/block'; -import { GasFees, GasSettings } from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { type BlockHeader, CallContext, type Capsule, TxContext } from '@aztec/stdlib/tx'; @@ -44,7 +45,10 @@ describe('PrivateExecutionOracle', () => { txContext = TxContext.from({ chainId: new Fr(10), version: new Fr(20), - gasSettings: GasSettings.fallback({ maxFeesPerGas: new GasFees(10, 10) }), + gasSettings: GasSettings.fallback({ + gasLimits: new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS), + maxFeesPerGas: new GasFees(10, 10), + }), }); }); diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index fe7e18aaf9cf..a646039c3da0 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -87,6 +87,7 @@ describe('PXE', () => { multiCallEntrypoint: await AztecAddress.random(), }, realProofs: true, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, }); pxe = await PXE.create({ diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/L2TipsKVStore.json b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/L2TipsKVStore.json index bce86863558b..110116f9521c 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/L2TipsKVStore.json +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/L2TipsKVStore.json @@ -9,12 +9,18 @@ "value": "num:179" }, { - "key": "utf8:proposedCheckpoint", - "value": "num:71" + "key": "utf8:proven", + "value": "num:79" + } + ], + "pxe_l2_tip_checkpoints": [ + { + "key": "utf8:checkpointed", + "value": "{\"number\":47,\"hash\":\"0x00a21eaf943186f15f609a8a76fb0bee7ce3e78c86ef95f5466614e59546eb19\"}" }, { "key": "utf8:proven", - "value": "num:79" + "value": "{\"number\":47,\"hash\":\"0x0000000000000000000000000000000000000000000000000000000000000059\"}" } ], "pxe_l2_block_hashes": [ @@ -30,17 +36,5 @@ "key": "num:79", "value": "utf8:0x0000000000000000000000000000000000000000000000000000000000000053" } - ], - "pxe_l2_block_number_to_checkpoint_number": [ - { - "key": "num:179", - "value": "num:47" - } - ], - "pxe_l2_checkpoint_store": [ - { - "key": "num:47", - "value": "00000000000000000000000000000000000000000000000000000000000000020000000300000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000130000000000000017000000000000000000000000000000000000001d000000000000000000000000000000000000000000000000000000000000001f0000000000000000000000000000002500000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002b00000001000000000000000000000000000000000000000000000000000000000000006b0000006d00000000000000000000000000000000000000000000000000000000000000710000007f000000000000000000000000000000000000000000000000000000000000008300000089000000000000000000000000000000000000000000000000000000000000008b0000009500000000000000000000000000000000000000000000000000000000000000970000009d00000000000000000000000000000000000000000000000000000000000000a300000000000000000000000000000000000000000000000000000000000000a700000000000000000000000000000000000000000000000000000000000000ad000000b3000000b500000000000000bf00000000000000000000000000000000000000c100000000000000000000000000000000000000000000000000000000000000c5000000000000000000000000000000c7000000000000000000000000000000d300000000000000000000000000000000000000000000000000000000000000df00000000000000000000000000000000000000000000000000000000000000e3000000000000000000000000000000000000000000000000000000000000006500000067000000010100000000000000000000000000000000000000000000000000000000000000e500000000000000000000000000000000000000000000000000000000000000e90100000000000000000000000000000000000000000000000000000000000000ef0100000000000000000000000000000000000000000000000000000000000000f10100000000000000000000000000000000000000000000000000000000000000fb010000000000000000000000000000000000000000000000000000000000000101000000000000000000000000000000000000000000000000000000000000010701000000000000000000000000000000000000000000000000000000000000010d000000000000000000000000000000000000000000000000000000000000010f000000000000000000000000000000000000000000000000000000000000011500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000001000000020000000000000000000000000000000000000000000000000000000000000119000000000000000000000000000000000000000000000000000000000000011b000000000000000000000000000000000000000000000000000000000000012501000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000001370000000000000000000000000000000000000000000000000000000000000139000000000000000000000000000000000000000000000000000000000000013d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000014b000001510000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b00000042307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303433000000000000000000000000000000000000000000000000000000000000003d00000000" - } ] } diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/opened_stores.json b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/opened_stores.json index 6c031f74812f..0f52df771e6c 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/opened_stores.json +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/opened_stores.json @@ -1,5 +1,5 @@ { - "schemaVersion": 7, + "schemaVersion": 8, "stores": [ { "name": "address_book", @@ -90,11 +90,7 @@ "kind": "map" }, { - "name": "pxe_l2_block_number_to_checkpoint_number", - "kind": "map" - }, - { - "name": "pxe_l2_checkpoint_store", + "name": "pxe_l2_tip_checkpoints", "kind": "map" }, { diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/schema_tests.ts b/yarn-project/pxe/src/storage/backwards_compatibility_tests/schema_tests.ts index 9a1e54a2a2cf..e6e6fc8ac71d 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/schema_tests.ts +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/schema_tests.ts @@ -256,8 +256,7 @@ export const SCHEMA_TESTS: readonly SchemaTest[] = [ ); // `'blocks-added'` writes to `pxe_l2_tips` (proposed tag) and `pxe_l2_block_hashes`. - // `'chain-checkpointed'` writes to all four sub-stores: tips ('checkpointed' and 'proposedCheckpoint' tags), - // block-to-checkpoint mapping, and the checkpoint store. + // `'chain-checkpointed'` writes the 'checkpointed' tip and its checkpoint id (`pxe_l2_tip_checkpoints`). await l2TipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); await l2TipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', @@ -269,15 +268,15 @@ export const SCHEMA_TESTS: readonly SchemaTest[] = [ await l2TipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: { number: BlockNumber(79), hash: new Fr(83n).toString() }, + checkpoint: { number: CheckpointNumber(47), hash: new Fr(89n).toString() }, }); }, snapshotStore: async kvStore => ({ pxe_l2_tips: await snapshotMap(kvStore.openMap('pxe_l2_tips')), - pxe_l2_block_hashes: await snapshotMap(kvStore.openMap('pxe_l2_block_hashes')), - pxe_l2_block_number_to_checkpoint_number: await snapshotMap( - kvStore.openMap('pxe_l2_block_number_to_checkpoint_number'), + pxe_l2_tip_checkpoints: await snapshotMap( + kvStore.openMap('pxe_l2_tip_checkpoints'), ), - pxe_l2_checkpoint_store: await snapshotMap(kvStore.openMap('pxe_l2_checkpoint_store')), + pxe_l2_block_hashes: await snapshotMap(kvStore.openMap('pxe_l2_block_hashes')), }), }, diff --git a/yarn-project/pxe/src/storage/metadata.ts b/yarn-project/pxe/src/storage/metadata.ts index 4136adf327e2..e242d8c59327 100644 --- a/yarn-project/pxe/src/storage/metadata.ts +++ b/yarn-project/pxe/src/storage/metadata.ts @@ -1 +1 @@ -export const PXE_DATA_SCHEMA_VERSION = 7; +export const PXE_DATA_SCHEMA_VERSION = 8; diff --git a/yarn-project/sequencer-client/README.md b/yarn-project/sequencer-client/README.md index 1bda73893c9b..5b709095ce54 100644 --- a/yarn-project/sequencer-client/README.md +++ b/yarn-project/sequencer-client/README.md @@ -245,8 +245,7 @@ The configuration object is `SequencerConfig` (`src/sequencer/config.ts` + `src/ | Option / env var | Default | Purpose | | --- | --- | --- | -| `blockDurationMs` / `SEQ_BLOCK_DURATION_MS` | unset | Length of one sub-slot in ms. `undefined` falls back to single-block-per-slot mode (used by tests / sandbox). | -| `enforceTimeTable` / `SEQ_ENFORCE_TIME_TABLE` | true | If false, deadlines are not enforced and a single block is built with unbounded time. | +| `blockDurationMs` / `SEQ_BLOCK_DURATION_MS` | 3000 ms | Length of one sub-slot in ms. Required: the sequencer always runs the enforced timetable. The derived `maxBlocksPerCheckpoint = floor((aztecSlotDuration − checkpointInitializationTime − (checkpointAssembleTime + 2·p2pPropagationTime + blockDuration)) / blockDuration)`; a slot may legitimately fit a single block when that floor is 1. | | `attestationPropagationTime` / `SEQ_ATTESTATION_PROPAGATION_TIME` | 2 s | One-way p2p estimate fed to the timetable. | | `sequencerPollingIntervalMS` / `SEQ_POLLING_INTERVAL_MS` | 500 | Work-loop tick rate. | diff --git a/yarn-project/sequencer-client/package.json b/yarn-project/sequencer-client/package.json index 22b4af6455cb..5ec46cf77e54 100644 --- a/yarn-project/sequencer-client/package.json +++ b/yarn-project/sequencer-client/package.json @@ -4,6 +4,7 @@ "type": "module", "exports": { ".": "./dest/index.js", + "./automine": "./dest/sequencer/automine/index.js", "./config": "./dest/config.js", "./test": "./dest/test/index.js" }, diff --git a/yarn-project/sequencer-client/src/config.ts b/yarn-project/sequencer-client/src/config.ts index 0bcb5ef6cb80..21939f61deef 100644 --- a/yarn-project/sequencer-client/src/config.ts +++ b/yarn-project/sequencer-client/src/config.ts @@ -14,11 +14,13 @@ import { type P2PConfig, p2pConfigMappings } from '@aztec/p2p/config'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ChainConfig, + DEFAULT_BLOCK_DURATION_MS, DEFAULT_MAX_BLOCKS_PER_CHECKPOINT, type SequencerConfig, chainConfigMappings, sharedSequencerConfigMappings, } from '@aztec/stdlib/config'; +import { MIN_PER_BLOCK_ALLOCATION_MULTIPLIER, MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER } from '@aztec/stdlib/gas'; import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server'; import { DEFAULT_P2P_PROPAGATION_TIME } from '@aztec/stdlib/timetable'; import { type ValidatorClientConfig, validatorClientConfigMappings } from '@aztec/validator-client/config'; @@ -42,9 +44,12 @@ export const DefaultSequencerConfig = { minTxsPerBlock: 1, buildCheckpointIfEmpty: false, publishTxsWithProposals: false, - perBlockAllocationMultiplier: 1.2, + perBlockAllocationMultiplier: MIN_PER_BLOCK_ALLOCATION_MULTIPLIER, + perBlockDAAllocationMultiplier: MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER, redistributeCheckpointBudget: true, - enforceTimeTable: true, + blockDurationMs: DEFAULT_BLOCK_DURATION_MS, + l1PublishingTime: 12, + checkpointProposalSyncGraceSeconds: 2 * (DEFAULT_BLOCK_DURATION_MS / 1000), attestationPropagationTime: DEFAULT_P2P_PROPAGATION_TIME, secondsBeforeInvalidatingBlockAsCommitteeMember: 144, // 12 L1 blocks secondsBeforeInvalidatingBlockAsNonCommitteeMember: 432, // 36 L1 blocks @@ -118,6 +123,13 @@ export const sequencerConfigMappings: ConfigMappingsType = { ' Values greater than one allow early blocks to use more than their even share, relying on checkpoint-level capping for later blocks.', ...numberConfigHelper(DefaultSequencerConfig.perBlockAllocationMultiplier), }, + perBlockDAAllocationMultiplier: { + env: 'SEQ_PER_BLOCK_DA_ALLOCATION_MULTIPLIER', + description: + 'Per-block budget multiplier applied to DA gas and blob fields in place of perBlockAllocationMultiplier.' + + ' Defaults higher than the general multiplier so the largest contract class deploy fits a single block.', + ...numberConfigHelper(DefaultSequencerConfig.perBlockDAAllocationMultiplier), + }, redistributeCheckpointBudget: { env: 'SEQ_REDISTRIBUTE_CHECKPOINT_BUDGET', description: @@ -142,16 +154,16 @@ export const sequencerConfigMappings: ConfigMappingsType = { env: 'ACVM_BINARY_PATH', description: 'The path to the ACVM binary', }, - enforceTimeTable: { - env: 'SEQ_ENFORCE_TIME_TABLE', - description: 'Whether to enforce the time table when building blocks', - ...booleanConfigHelper(DefaultSequencerConfig.enforceTimeTable), - }, governanceProposerPayload: { env: 'GOVERNANCE_PROPOSER_PAYLOAD_ADDRESS', description: 'The address of the payload for the governanceProposer', parseEnv: (val: string) => EthAddress.fromString(val), }, + l1PublishingTime: { + env: 'SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT', + description: 'How much time in seconds to allow in the slot for publishing the L1 transaction.', + ...numberConfigHelper(DefaultSequencerConfig.l1PublishingTime), + }, fakeProcessingDelayPerTxMs: { description: 'Used for testing to introduce a fake delay after processing each tx', }, diff --git a/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.test.ts b/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.test.ts new file mode 100644 index 000000000000..784d5f2e35f4 --- /dev/null +++ b/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.test.ts @@ -0,0 +1,44 @@ +import { GasFees, ManaUsageEstimate } from '@aztec/stdlib/gas'; + +import { jest } from '@jest/globals'; + +import { FeeProviderImpl } from './fee_provider.js'; + +describe('FeeProviderImpl', () => { + function makeProvider(currentMinFees: GasFees, predictedMinFees: GasFees[]) { + const blockNumber = 1n; + const getBlockNumber = jest.fn<() => Promise>(() => Promise.resolve(blockNumber)); + const getPredictedMinFees = jest.fn<(manaUsage: ManaUsageEstimate) => Promise>(() => + Promise.resolve(predictedMinFees), + ); + const provider: FeeProviderImpl = Object.create(FeeProviderImpl.prototype); + + Reflect.set(provider, 'publicClient', { getBlockNumber }); + Reflect.set(provider, 'currentL1BlockNumber', blockNumber); + Reflect.set(provider, 'currentMinFees', Promise.resolve(currentMinFees)); + Reflect.set(provider, 'feePredictor', { getPredictedMinFees }); + + return { provider, getBlockNumber, getPredictedMinFees }; + } + + it('prepends current min fees to predicted future fees', async () => { + const currentMinFees = new GasFees(1, 2); + const predictedMinFees = [new GasFees(3, 4), new GasFees(5, 6)]; + const { provider, getBlockNumber, getPredictedMinFees } = makeProvider(currentMinFees, predictedMinFees); + + await expect(provider.getPredictedMinFees(ManaUsageEstimate.Limit)).resolves.toEqual([ + currentMinFees, + ...predictedMinFees, + ]); + expect(getBlockNumber).toHaveBeenCalledWith({ cacheTime: 0 }); + expect(getPredictedMinFees).toHaveBeenCalledWith(ManaUsageEstimate.Limit); + }); + + it('defaults future fee prediction to target mana usage', async () => { + const { provider, getPredictedMinFees } = makeProvider(new GasFees(1, 2), [new GasFees(3, 4)]); + + await provider.getPredictedMinFees(); + + expect(getPredictedMinFees).toHaveBeenCalledWith(ManaUsageEstimate.Target); + }); +}); diff --git a/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.ts b/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.ts index 19d1d74b9382..3ec4cb194d7d 100644 --- a/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.ts +++ b/yarn-project/sequencer-client/src/global_variable_builder/fee_provider.ts @@ -59,7 +59,7 @@ export class FeeProviderImpl implements FeeProvider { public async getCurrentMinFees(): Promise { // Get the current block number - const blockNumber = await this.publicClient.getBlockNumber(); + const blockNumber = await this.publicClient.getBlockNumber({ cacheTime: 0 }); // If the L1 block number has changed then chain a new promise to get the current min fees if (this.currentL1BlockNumber === undefined || blockNumber > this.currentL1BlockNumber) { @@ -69,7 +69,12 @@ export class FeeProviderImpl implements FeeProvider { return this.currentMinFees; } - public getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise { - return this.feePredictor.getPredictedMinFees(manaUsage ?? ManaUsageEstimate.Target); + public async getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise { + const [currentMinFees, predictedMinFees] = await Promise.all([ + this.getCurrentMinFees(), + this.feePredictor.getPredictedMinFees(manaUsage ?? ManaUsageEstimate.Target), + ]); + + return [currentMinFees, ...predictedMinFees]; } } diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts index 7b156d567f79..2375b9013a90 100644 --- a/yarn-project/sequencer-client/src/index.ts +++ b/yarn-project/sequencer-client/src/index.ts @@ -1,16 +1,7 @@ export * from './client/index.js'; export * from './config.js'; export * from './publisher/index.js'; -export { - AutomineSequencer, - type AutomineSequencerConstants, - type AutomineSequencerDeps, - createAutomineSequencer, - type CreateAutomineSequencerArgs, - Sequencer, - SequencerState, - type SequencerEvents, -} from './sequencer/index.js'; +export { Sequencer, SequencerState, type SequencerEvents } from './sequencer/index.js'; // Used by the node to simulate public parts of transactions. Should these be moved to a shared library? // ISSUE(#9832) diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts index bf7b7f2fafb8..519fbdb6dfed 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts @@ -720,6 +720,28 @@ describe('SequencerPublisher', () => { expect((publisher as any).requests.length).toEqual(0); }); + it('does not sleep in sendRequestsAt if interrupted beforehand', async () => { + // A target slot far enough in the future that sendRequestsAt would sleep for ~1 hour + // (EmptyL1RollupConstants has slotDuration 1s and l1GenesisTime 0, so slot N starts at N seconds). + const targetSlot = SlotNumber(Math.ceil(Date.now() / 1000) + 3600); + publisher.interrupt(); + + let timeout: NodeJS.Timeout | undefined; + try { + const result = await Promise.race([ + publisher.sendRequestsAt(targetSlot), + new Promise<'timed-out'>(resolve => { + timeout = setTimeout(() => resolve('timed-out'), 1000); + }), + ]); + expect(result).toBeUndefined(); + } finally { + if (timeout) { + clearTimeout(timeout); + } + } + }); + it('does not send requests if no valid requests are found', async () => { publisher.addRequest({ action: 'propose', diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 96754bfeea39..738e83d5d5b5 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -630,6 +630,9 @@ export class SequencerPublisher { // Aim to be in the mempool one L1 slot before the L2 slot starts, so we have a chance of // being picked up by the first L1 block of the L2 slot. const submitAfterMs = startOfTargetSlotMs - Number(this.ethereumSlotDuration) * 1000; + if (this.interrupted) { + return undefined; + } const sleepMs = submitAfterMs - this.dateProvider.now(); if (sleepMs > 0) { this.log.debug(`Sleeping ${sleepMs}ms before sending requests`, { diff --git a/yarn-project/sequencer-client/src/sequencer/automine/README.md b/yarn-project/sequencer-client/src/sequencer/automine/README.md index baabfd7aaf53..0eecafddc3ee 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/README.md +++ b/yarn-project/sequencer-client/src/sequencer/automine/README.md @@ -19,11 +19,11 @@ Compared to the production `Sequencer`: - No validator orchestration, attestation collection, or P2P proposal gossip. - No slashing, no governance votes, no `SequencerEvents`. -Consumers (archiver, world-state, `EpochTestSettler`) observe L1 and the archiver tip directly rather than listening for sequencer events. +Consumers (archiver, world-state) observe L1 and the archiver tip directly rather than listening for sequencer events. ## Serial-queue invariant -Every operation — mempool-driven block builds, explicit empty-block builds, time warps, and reorgs — is serialized through a single `SerialQueue`. They never interleave. +Every operation — mempool-driven block builds, explicit empty-block builds, time warps, reorgs, and synthetic epoch proving — is serialized through a single `SerialQueue`. They never interleave. Public entry points: @@ -32,15 +32,29 @@ Public entry points: | `buildIfPending()` | Enqueues a mempool-driven build. Coalesces bursts into one job. | | `buildEmptyBlock()` | Enqueues a forced empty-block build. | | `warpTo(ts)` / `warpBy(delta)` | Advances L1 time to a slot boundary. | +| `prove(upToCheckpoint?)` | Synthetically proves epochs up to a checkpoint (default: the latest checkpointed): writes the epoch out hashes into the L1 Outbox so L2-to-L1 messages become consumable, then advances the proven tip. No real proof. Clamps to the checkpointed tip and no-ops when already proven. | | `revertToCheckpoint(n)` | Rolls L1 back to the block that published checkpoint `n`, then resets archiver, world-state, and P2P pool. | | `syncPoint()` | Awaits the queue reaching idle. | +## Time control + +The AutomineSequencer owns L1 time in the local network (replacing the deleted `AnvilTestWatcher`). It builds and publishes each checkpoint at the next aztec-slot boundary, and `warpTo` / `warpBy` advance the clock by publishing an empty checkpoint at the target slot. Before every build, warp, and prove it reconciles the injected `TestDateProvider` to the latest *mined* L1 timestamp, so node-side consumers of `dateProvider.now()` stay aligned with L1 even when an unrelated L1 tx mines a block between our builds. It never advances the clock to the pending, un-mined timestamp. + +## Publish-failure recovery + +A failed propose mines no checkpoint on L1 (it reverts inside the multicall or is never sent), so recovery is purely local — there is **no L1 reorg**. The optimistic archiver insert (the proposed block plus its proposed checkpoint) is rolled back via `archiver.removeUncheckpointedBlocksAfter`, which removes the uncheckpointed block and evicts the proposed checkpoint that referenced it. `p2pClient.sync()` then observes the lowered proposed tip and returns the block's txs to the pending pool, `worldState.syncImmediate()` drops any applied effects, and the L1 nonce is reset. The build is not retried inline; the mempool poller re-enqueues one once the txs are back in the pool. + ## Entry points **Factory** — `createAutomineSequencer` in `automine_factory.ts` wires up all dependencies (publisher manager, keystore, cheat codes, etc.), starts the `PublisherManager`, and returns an unstarted `AutomineSequencer`. The caller (`AztecNodeService.createAndSync` in `aztec-node/src/aztec-node/server.ts`) invokes `AutomineSequencer.start()` separately. It is called by the full node when `aztecTargetCommitteeSize == 0`. **Test fixture** — `AUTOMINE_E2E_OPTS` in `end-to-end/src/fixtures/fixtures.ts` is the test-side entry point. Pass it to `setup()` to get a node + `AutomineSequencer` instead of the production sequencer stack. -## Epoch proving caveat +## Epoch proving + +There is no real prover in the automine setup, so epochs are settled synthetically: the epoch out hash is written into the L1 Outbox via cheat codes and the rollup's proven tip is advanced — the local-network equivalent of an epoch proof landing on L1. The grouping/out-hash logic lives in the shared `settleEpochOutbox` helper in `@aztec/prover-client/test`, used by both proving drivers below. + +Who drives proving depends on the `automineEnableProveEpoch` config flag: -Epoch proving remains manual under `AUTOMINE_E2E_OPTS`. The e2e `setup()` fixture does NOT wire an `EpochTestSettler` — that observer is only attached in `local-network.ts`. Tests that cross epoch boundaries must therefore advance the proven anchor explicitly via `cheatCodes.rollup.markAsProven(...)`. See `e2e_lending_contract.test.ts` (which calls `progressSlots` in `simulators/lending_simulator.ts`) and `e2e_pruned_blocks.test.ts` for real examples. +- **Local network / sandbox** (`automineEnableProveEpoch: true`): the AutomineSequencer runs an auto-prove loop that calls `prove()` as checkpoints land, through the same serial queue as its builds. This replaces the standalone `EpochTestSettler`, which used to race the build loop. The loop also reconciles the clock on each tick. +- **e2e tests** (`AUTOMINE_E2E_OPTS`, flag off): proving is manual so tests stay deterministic. Cross-epoch tests advance the proven anchor explicitly via `node.prove(...)`, `cheatCodes.rollup.markAsProven(...)`, or a hand-driven `EpochTestSettler`. See `e2e_pruned_blocks.test.ts` and `e2e_epochs/epochs_partial_proof_multi_root.test.ts` for real examples. diff --git a/yarn-project/sequencer-client/src/sequencer/automine/automine_factory.ts b/yarn-project/sequencer-client/src/sequencer/automine/automine_factory.ts index a67a8ff85102..bcabb261db19 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/automine_factory.ts +++ b/yarn-project/sequencer-client/src/sequencer/automine/automine_factory.ts @@ -40,9 +40,14 @@ export type CreateAutomineSequencerArgs = { worldStateSynchronizer: WorldStateSynchronizer; archiver: L2BlockSource & L1ToL2MessageSource & - Pick; + Pick< + Archiver, + 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter' + >; p2pClient: P2P & Pick; l1Constants: { l1GenesisTime: bigint; slotDuration: number; ethereumSlotDuration: number; rollupManaLimit: number }; + /** When true, run the auto-settle / clock-reconcile loop (local-network only). */ + autoSettle?: boolean; log: Logger; }; @@ -71,6 +76,7 @@ export async function createAutomineSequencer({ archiver, p2pClient, l1Constants, + autoSettle, log, }: CreateAutomineSequencerArgs): Promise { const publisherManager = new PublisherManager(l1TxUtils, getPublisherConfigFromSequencerConfig(config), { @@ -136,6 +142,7 @@ export async function createAutomineSequencer({ config, archiver, l1TxUtils: reorgResetL1TxUtils, + autoSettle, stopExtras: () => publisherManager.stop(), log: log.createChild('automine-sequencer'), }); diff --git a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts index 3da1735ee7b3..243175965897 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts @@ -1,7 +1,7 @@ import type { Archiver } from '@aztec/archiver'; import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils'; -import type { EthCheatCodes } from '@aztec/ethereum/test'; -import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { type EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; +import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import type { EthAddress } from '@aztec/foundation/eth-address'; import { Signature } from '@aztec/foundation/eth-signature'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -10,6 +10,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise'; import type { TestDateProvider } from '@aztec/foundation/timer'; import { isErrorClass } from '@aztec/foundation/types'; import type { P2PClient as ConcreteP2PClient, P2P } from '@aztec/p2p'; +import { settleEpochOutbox } from '@aztec/prover-client/test'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CommitteeAttestationsAndSigners, type L2Block, type L2BlockSource } from '@aztec/stdlib/block'; import { getPreviousCheckpointOutHashes } from '@aztec/stdlib/checkpoint'; @@ -65,7 +66,10 @@ export type AutomineSequencerDeps = { * Archiver used to push locally-built blocks and proposed checkpoints into the in-memory * store, force an immediate L1 sync after publishing, and roll back state during reorgs. */ - archiver: Pick; + archiver: Pick< + Archiver, + 'rollbackTo' | 'addBlock' | 'addProposedCheckpoint' | 'syncImmediate' | 'removeUncheckpointedBlocksAfter' + >; /** L1 tx utils whose cached nonces must be reset after an L1 reorg. */ l1TxUtils: Pick[]; /** @@ -75,6 +79,14 @@ export type AutomineSequencerDeps = { stopExtras?: () => Promise; /** How often to poll the mempool for new txs while running. Defaults to 50ms. */ pollIntervalMs?: number; + /** + * When true, run a loop that synthetically settles epochs (writes outbox roots and advances the + * proven tip) as checkpoints land, replacing the standalone `EpochTestSettler`. Local-network only; + * e2e tests leave this off and drive proving explicitly. + */ + autoSettle?: boolean; + /** How often the auto-settle / clock-reconcile loop runs while {@link autoSettle} is enabled. Defaults to 200ms. */ + settlePollIntervalMs?: number; log?: Logger; }; @@ -106,6 +118,8 @@ export class AutomineSequencer { private stopped = false; private paused = false; private mempoolPoller?: RunningPromise; + /** Loop that settles epochs and reconciles the clock while {@link AutomineSequencerDeps.autoSettle} is set. */ + private settler?: RunningPromise; private publisher?: SequencerPublisher; private attestorAddress?: EthAddress; @@ -115,6 +129,9 @@ export class AutomineSequencer { /** Last L2 slot we published a checkpoint for (-1 means none yet). */ private lastBuiltSlot: number = -1; + /** Lazily-built cheat codes for synthetic epoch settlement (outbox roots + proven tip). */ + private rollupCheatCodes?: RollupCheatCodes; + constructor(deps: AutomineSequencerDeps) { this.deps = deps; this.log = deps.log ?? createLogger('sequencer:automine'); @@ -122,6 +139,14 @@ export class AutomineSequencer { this.pollIntervalMs = deps.pollIntervalMs ?? 50; } + /** Cheat codes for the rollup, built from the same EthCheatCodes the sequencer uses for time control. */ + private getRollupCheatCodes(): RollupCheatCodes { + this.rollupCheatCodes ??= new RollupCheatCodes(this.deps.ethCheatCodes, { + rollupAddress: this.deps.config.rollupAddress, + }); + return this.rollupCheatCodes; + } + /** * Starts the sequencer. Switches anvil into automine mode (no interval mining), * acquires a publisher, and begins polling the mempool for pending txs. @@ -149,6 +174,13 @@ export class AutomineSequencer { // notification. Tests can also call `buildIfPending()` directly to skip the poll wait. this.mempoolPoller = new RunningPromise(() => this.maybeEnqueueBuild(), this.log, this.pollIntervalMs); this.mempoolPoller.start(); + + // Local-network only: settle epochs and reconcile the clock to L1 as checkpoints land. Both run + // through the same serial queue as builds/warps, so they never interleave with a checkpoint build. + if (this.deps.autoSettle) { + this.settler = new RunningPromise(() => this.maybeSettle(), this.log, this.deps.settlePollIntervalMs ?? 200); + this.settler.start(); + } } /** @@ -163,6 +195,7 @@ export class AutomineSequencer { this.stopped = true; this.running = false; await this.mempoolPoller?.stop(); + await this.settler?.stop(); await this.queue.end(); await this.deps.stopExtras?.(); this.log.info('AutomineSequencer stopped'); @@ -271,6 +304,50 @@ export class AutomineSequencer { return this.queue.syncPoint(); } + /** + * Synthetically "proves" the L2 chain up to `upToCheckpoint` (default: the latest checkpointed + * checkpoint). For every epoch newly covered — including a partial final epoch — the epoch out hash + * is written into the L1 Outbox so the L2-to-L1 messages in those checkpoints become consumable, + * then the rollup's proven tip is advanced to the target. There is no real proof; this is the + * local-network equivalent of an epoch proof landing on L1. + * + * Clamps the target down to the latest checkpointed checkpoint and no-ops when it is already proven. + * Runs inside the serial queue so it never interleaves with a build or warp. + * + * @returns The proven checkpoint after the call (the target, or the existing proven tip on no-op). + */ + public prove(upToCheckpoint?: CheckpointNumber): Promise { + return this.queue.put(() => this.runProve(upToCheckpoint)); + } + + /** + * Auto-settle tick (local-network only). Proving up to the latest checkpointed checkpoint also + * reconciles the clock at the head of {@link runProve}, so this single tick keeps both the + * proven tip and the date provider current even when no build is happening. + */ + private async maybeSettle(): Promise { + if (!this.running) { + return; + } + try { + await this.prove(); + } catch (err) { + this.log.warn('Automine auto-settle tick failed', { err: err instanceof Error ? err.message : String(err) }); + } + } + + /** + * Advances the injected date provider to the latest *mined* L1 timestamp when it has fallen behind + * (e.g. an unrelated L1 tx mined a block between our builds). Never advances to the pending, un-mined + * timestamp. Keeps node-side consumers of `dateProvider.now()` aligned with L1 without our own builds. + */ + private async reconcileDateProvider(): Promise { + const lastTsMs = (await this.deps.ethCheatCodes.lastBlockTimestamp()) * 1000; + if (lastTsMs > this.deps.dateProvider.now()) { + this.deps.dateProvider.setTime(lastTsMs); + } + } + /** Called from the mempool poller. Enqueues a build if there are pending txs. */ private async maybeEnqueueBuild(): Promise { if (!this.running || this.paused || this.buildQueued) { @@ -290,11 +367,19 @@ export class AutomineSequencer { } } - /** Builds one checkpoint with a single block, publishes it, syncs the date provider. */ + /** + * Builds and publishes one checkpoint, returning the built block — or `undefined` when there was + * nothing to build or the propose failed. A failed propose mines no checkpoint on L1 (it reverts + * inside the multicall or is never sent), so recovery is purely local: the optimistic archiver insert + * is rolled back, the block's txs return to the pending pool, and the L1 nonce is reset. No L1 reorg + * is performed, and the build is not retried inline — once the txs are back in the pending pool the + * mempool poller re-enqueues a build on its next tick (see {@link maybeEnqueueBuild}). + */ private async runBuild({ allowEmpty }: { allowEmpty: boolean }): Promise { if (!this.running || !this.publisher || !this.attestorAddress) { return undefined; } + await this.reconcileDateProvider(); const txCount = await this.deps.p2pClient.getPendingTxCount(); // For mempool-driven builds, wait for at least `minTxsPerBlock` pending txs (or 1 if not set) @@ -420,15 +505,16 @@ export class AutomineSequencer { // offset — automine is the deliberate non-pipelined exception and builds/publishes in place. const result = await this.publisher.sendRequests(SlotNumber(targetSlot)); - const successful = result?.successfulActions?.find(a => a === 'propose'); - if (!successful) { - this.log.error('Propose action did not succeed under automine', { + const proposeSucceeded = !!result?.successfulActions?.some(action => action === 'propose'); + if (!proposeSucceeded) { + this.log.warn('Automine propose did not succeed; rolled back optimistic insert, will rebuild from the poller', { slot: targetSlot, checkpointNumber, successful: result?.successfulActions, failed: result?.failedActions, }); - throw new Error(`AutomineSequencer: propose did not succeed for slot ${targetSlot}`); + await this.rollbackOptimisticInsert(syncedToBlockNumber); + return undefined; } // Force one full L1-sync cycle synchronously. The local addBlock/addProposedCheckpoint @@ -440,6 +526,17 @@ export class AutomineSequencer { const newL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp(); this.deps.dateProvider.setTime(newL1Ts * 1000); + // A successful propose is validated on-chain against the mined L1 block's slot, so the mined slot + // should equal our target; warn (without rolling back — the checkpoint is on L1) if it ever differs. + const minedSlot = Number(getSlotAtTimestamp(BigInt(newL1Ts), this.deps.l1Constants)); + if (minedSlot !== targetSlot) { + this.log.warn(`Automine checkpoint mined in slot ${minedSlot} but targeted ${targetSlot}`, { + minedSlot, + targetSlot, + checkpointNumber, + }); + } + this.lastBuiltSlot = targetSlot; this.log.verbose(`Automine checkpoint published`, { @@ -453,6 +550,23 @@ export class AutomineSequencer { return buildResult.block; } + /** + * Undoes an optimistic archiver insert (uncheckpointed block + proposed checkpoint) without reorging + * L1: removes the uncheckpointed block and evicts the proposed checkpoint that referenced it, so the + * proposed tip drops back to `toBlockNumber`. `p2pClient.sync()` then observes the lowered tip and + * restores the block's txs to the pending pool; `worldState.syncImmediate()` drops any applied effects; + * and the L1 nonce is reset (a reverted-but-mined propose consumes one) so the build can be retried. + * + * Note: `archiver.rollbackTo` is NOT usable here — it is checkpoint-granular and no-ops on a + * proposed-only tip (the inserted checkpoint is not yet in the checkpointed set). + */ + private async rollbackOptimisticInsert(toBlockNumber: BlockNumber): Promise { + await this.deps.archiver.removeUncheckpointedBlocksAfter(toBlockNumber); + await this.deps.p2pClient.sync(); + await this.deps.worldState.syncImmediate(); + this.deps.l1TxUtils.forEach(utils => utils.resetNonce()); + } + /** * Warps L1 timestamp to (or past) `targetTimestampSec`, rounded up to the next aztec-slot * boundary, by queuing an empty-checkpoint build at that slot. Mines exactly one L1 block @@ -460,6 +574,7 @@ export class AutomineSequencer { * the mined block lands on the slot boundary. */ private async runWarp(targetTimestampSec: number): Promise { + await this.reconcileDateProvider(); const currentL1Ts = await this.deps.ethCheatCodes.lastBlockTimestamp(); if (targetTimestampSec <= currentL1Ts) { this.log.debug(`Warp target ${targetTimestampSec} is not in the future of current L1 ts ${currentL1Ts}`); @@ -537,6 +652,78 @@ export class AutomineSequencer { }); } + /** + * Writes outbox roots for every epoch newly covered up to `maybeCheckpoint` and advances the proven + * tip. Settles each fully-covered epoch in full and the final epoch only up to the target checkpoint + * (a partial epoch, which the AZIP-14 Outbox supports via per-`numCheckpointsInEpoch` roots). + */ + private async runProve(upToCheckpoint?: CheckpointNumber): Promise { + await this.reconcileDateProvider(); + const rollupCheatCodes = this.getRollupCheatCodes(); + const tips = await this.deps.l2BlockSource.getL2Tips(); + const checkpointedTip = tips.checkpointed.checkpoint.number; + + // Never prove beyond what the archiver has actually checkpointed; default to that tip. + const target = CheckpointNumber(Math.min(upToCheckpoint ?? checkpointedTip, checkpointedTip)); + + const { proven } = await rollupCheatCodes.getTips(); + if (target <= proven) { + this.log.debug(`Checkpoint ${target} already proven`, { target, proven, checkpointedTip }); + return proven; + } + + const startEpoch = await this.getEpochOfCheckpoint(CheckpointNumber(proven + 1)); + const endEpoch = await this.getEpochOfCheckpoint(target); + if (startEpoch === undefined || endEpoch === undefined) { + this.log.warn(`Cannot resolve epoch range to prove up to checkpoint ${target}`, { + target, + proven, + startEpoch, + endEpoch, + }); + return proven; + } + + for (let epoch = startEpoch; epoch <= endEpoch; epoch++) { + const lastCovered = await settleEpochOutbox({ + rollupCheatCodes, + l2BlockSource: this.deps.l2BlockSource, + epoch: EpochNumber(epoch), + maxCheckpoint: epoch === endEpoch ? target : undefined, + log: this.log, + }); + if (lastCovered === undefined) { + // An epoch in (proven, target] with no checkpointed blocks — expected when warps skip a whole + // epoch. Logged so it's distinguishable from the archiver failing to serve the epoch's blocks. + this.log.debug(`No checkpointed blocks to settle for epoch ${epoch} while proving to ${target}`, { + epoch, + target, + }); + } + } + + await rollupCheatCodes.markAsProven(target); + // Settlement is a direct L1 storage write that mines no block, unlike a real epoch proof landing + // on L1. The archiver's L1 sync short-circuits while the L1 block hash is unchanged, so it would + // never re-read the proven tip until the next build/warp mines a block. Mine one empty L1 block so + // the block hash advances, then force an immediate sync that observes the new proven checkpoint. + await this.deps.ethCheatCodes.mineEmptyBlock(); + await this.deps.archiver.syncImmediate(); + + this.log.verbose(`Proved up to checkpoint ${target}`, { target, proven, startEpoch, endEpoch }); + return target; + } + + /** Resolves the epoch a checkpoint belongs to from its slot, or undefined if the archiver lacks it. */ + private async getEpochOfCheckpoint(checkpointNumber: CheckpointNumber): Promise { + const checkpointData = await this.deps.l2BlockSource.getCheckpointData({ number: checkpointNumber }); + if (!checkpointData) { + return undefined; + } + const slot = SlotNumber(Number(checkpointData.header.slotNumber)); + return Number(getEpochAtSlot(slot, this.deps.l1Constants)); + } + /** * Wraps `checkpointBuilder.buildBlock` with the failed-tx handling shared by both error * and success paths: drops the failed txs from the P2P mempool, and returns `undefined` diff --git a/yarn-project/sequencer-client/src/sequencer/automine/index.ts b/yarn-project/sequencer-client/src/sequencer/automine/index.ts new file mode 100644 index 000000000000..8d4411baa249 --- /dev/null +++ b/yarn-project/sequencer-client/src/sequencer/automine/index.ts @@ -0,0 +1,6 @@ +export { + AutomineSequencer, + type AutomineSequencerConstants, + type AutomineSequencerDeps, +} from './automine_sequencer.js'; +export { createAutomineSequencer, type CreateAutomineSequencerArgs } from './automine_factory.js'; diff --git a/yarn-project/sequencer-client/src/sequencer/chain_state_overrides.ts b/yarn-project/sequencer-client/src/sequencer/chain_state_overrides.ts index 60c588faa974..0230fa90559b 100644 --- a/yarn-project/sequencer-client/src/sequencer/chain_state_overrides.ts +++ b/yarn-project/sequencer-client/src/sequencer/chain_state_overrides.ts @@ -1,4 +1,9 @@ -import { RollupContract, SimulationOverridesBuilder, type SimulationOverridesPlan } from '@aztec/ethereum/contracts'; +import { + type FeeHeader, + RollupContract, + SimulationOverridesBuilder, + type SimulationOverridesPlan, +} from '@aztec/ethereum/contracts'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import type { Logger } from '@aztec/foundation/log'; import { type ProposedCheckpointData, computeCheckpointPayloadDigest } from '@aztec/stdlib/checkpoint'; @@ -135,7 +140,9 @@ type PipelinedParentFeeHeaderInput = { * checkpoint); all other failure modes (missing grandparent state, missing fee header, RPC * errors) throw so callers don't silently desync the fee-header override. */ -export async function computePipelinedParentFeeHeader(input: PipelinedParentFeeHeaderInput) { +export async function computePipelinedParentFeeHeader( + input: PipelinedParentFeeHeaderInput, +): Promise { if (input.checkpointNumber < 2) { return undefined; } diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts index 7f0feed2fec0..f1da2c37dcee 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts @@ -14,6 +14,7 @@ import { TimeoutError } from '@aztec/foundation/error'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Signature } from '@aztec/foundation/eth-signature'; import { createLogger } from '@aztec/foundation/log'; +import { promiseWithResolvers } from '@aztec/foundation/promise'; import { TestDateProvider } from '@aztec/foundation/timer'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import { type P2P, P2PClientState } from '@aztec/p2p'; @@ -332,7 +333,6 @@ describe('CheckpointProposalJob', () => { config = { ...DefaultSequencerConfig, - enforceTimeTable: true, maxTxsPerBlock: 4, minTxsPerBlock: 1, publishTxsWithProposals: false, @@ -347,7 +347,6 @@ describe('CheckpointProposalJob', () => { timetable = makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, }); job = createCheckpointProposalJob(); @@ -375,11 +374,11 @@ describe('CheckpointProposalJob', () => { describe('single block mode', () => { beforeEach(() => { - // Single block mode: no blockDurationMs set + // Single block mode: a 9s block duration in a 24s slot derives exactly one block sub-slot. job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); }); @@ -390,6 +389,8 @@ describe('CheckpointProposalJob', () => { validatorClient.collectAttestations.mockResolvedValue(getAttestations(block)); + // Start building at the build-frame opening so the single block sub-slot is still selectable. + dateProvider.setTime(buildFrameStartSeconds() * 1000); const checkpoint = await job.executeAndAwait(); expect(checkpoint).toBeDefined(); @@ -463,6 +464,8 @@ describe('CheckpointProposalJob', () => { job.updateConfig({ buildCheckpointIfEmpty: true, minTxsPerBlock: 1 }); + // Start building at the build-frame opening so the single block sub-slot is still selectable. + dateProvider.setTime(buildFrameStartSeconds() * 1000); const checkpoint = await job.executeAndAwait(); expect(checkpoint).toBeDefined(); @@ -484,6 +487,8 @@ describe('CheckpointProposalJob', () => { validatorClient.collectAttestations.mockResolvedValue(getAttestations(block)); + // Start building at the build-frame opening so the single block sub-slot is still selectable. + dateProvider.setTime(buildFrameStartSeconds() * 1000); await job.executeAndAwait(); expect(validatorClient.collectAttestations).toHaveBeenCalledTimes(1); @@ -506,7 +511,7 @@ describe('CheckpointProposalJob', () => { job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); @@ -541,7 +546,7 @@ describe('CheckpointProposalJob', () => { job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); @@ -583,7 +588,7 @@ describe('CheckpointProposalJob', () => { job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); @@ -626,7 +631,7 @@ describe('CheckpointProposalJob', () => { job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); @@ -670,7 +675,7 @@ describe('CheckpointProposalJob', () => { job.setTimetable( makeProposerTimetable({ l1Constants, - enforce: config.enforceTimeTable, + blockDurationMs: 9000, }), ); @@ -1336,7 +1341,6 @@ describe('CheckpointProposalJob', () => { makeProposerTimetable({ l1Constants, blockDurationMs: 3000, - enforce: true, }), ); }); @@ -1606,6 +1610,13 @@ describe('CheckpointProposalJob', () => { }); describe('timing edge cases', () => { + beforeEach(() => { + // Single-block timetable started at the build-frame opening, so the real timetable selects exactly + // one block. Tests that mock selectNextSubslot below override this. + job.setTimetable(makeProposerTimetable({ l1Constants, blockDurationMs: 9000 })); + dateProvider.setTime(buildFrameStartSeconds() * 1000); + }); + it('handles insufficient time remaining in slot', async () => { // Mock selectNextSubslot to return false (not enough time) jest.spyOn(job.getTimetable(), 'selectNextSubslot').mockReturnValue(noSubslot()); @@ -1664,6 +1675,12 @@ describe('CheckpointProposalJob', () => { }); describe('error handling', () => { + beforeEach(() => { + // Single-block timetable started at the build-frame opening, so the real timetable selects exactly one block. + job.setTimetable(makeProposerTimetable({ l1Constants, blockDurationMs: 9000 })); + dateProvider.setTime(buildFrameStartSeconds() * 1000); + }); + it('handles block build failure gracefully', async () => { const txs = await Promise.all([makeTx(1, chainId)]); p2p.getPendingTxCount.mockResolvedValue(txs.length); @@ -1719,6 +1736,39 @@ describe('CheckpointProposalJob', () => { } }); + it('interrupts a pending L1 submission sleeping in the publisher', async () => { + const { txs, block } = await setupTxsAndBlock(p2p, globalVariables, 1, chainId); + checkpointBuilder.seedBlocks([block], [txs]); + validatorClient.collectAttestations.mockResolvedValue(getAttestations(block)); + + // Simulate sendRequestsAt sleeping until the target slot: the promise only resolves once + // the publisher itself is interrupted. + const sendDeferred = promiseWithResolvers(); + publisher.sendRequestsAt.mockReturnValue(sendDeferred.promise); + publisher.interrupt.mockImplementation(() => sendDeferred.resolve(undefined)); + + const checkpoint = await job.execute(); + expect(checkpoint).toBeDefined(); + + const pendingSubmission = job.awaitPendingSubmission().then(() => 'stopped' as const); + job.interrupt(); + + let timeout: NodeJS.Timeout | undefined; + try { + const result = await Promise.race([ + pendingSubmission, + new Promise<'timed-out'>(resolve => { + timeout = setTimeout(() => resolve('timed-out'), 1000); + }), + ]); + expect(result).toBe('stopped'); + } finally { + if (timeout) { + clearTimeout(timeout); + } + } + }); + it('aborts checkpoint when syncing proposed block to archiver fails', async () => { const { txs, block } = await setupTxsAndBlock(p2p, globalVariables, 1, chainId); checkpointBuilder.seedBlocks([block], [txs]); @@ -1771,6 +1821,12 @@ describe('CheckpointProposalJob', () => { }); describe('attestation collection', () => { + beforeEach(() => { + // Single-block timetable started at the build-frame opening, so the real timetable selects exactly one block. + job.setTimetable(makeProposerTimetable({ l1Constants, blockDurationMs: 9000 })); + dateProvider.setTime(buildFrameStartSeconds() * 1000); + }); + it('collects attestations in normal flow', async () => { const { txs, block } = await setupTxsAndBlock(p2p, globalVariables, 1, chainId); checkpointBuilder.seedBlocks([block], [txs]); @@ -1808,7 +1864,6 @@ describe('CheckpointProposalJob', () => { makeProposerTimetable({ l1Constants, blockDurationMs: 3000, - enforce: true, }), ); @@ -1847,7 +1902,6 @@ describe('CheckpointProposalJob', () => { makeProposerTimetable({ l1Constants, blockDurationMs: 3000, - enforce: true, }), ); diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts index 585f0abc7cc3..776929074d3c 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts @@ -386,7 +386,6 @@ describe('CheckpointProposalJob Timing Tests', () => { p2pPropagationTime: P2P_PROPAGATION_TIME, checkpointProposalPrepareTime: CHECKPOINT_ASSEMBLE_TIME, blockDurationMs: BLOCK_DURATION * 1000, - enforce: true, }); // Create timing-aware checkpoint builder @@ -493,7 +492,6 @@ describe('CheckpointProposalJob Timing Tests', () => { config = { ...DefaultSequencerConfig, - enforceTimeTable: true, maxTxsPerBlock: 4, minTxsPerBlock: 1, publishTxsWithProposals: false, @@ -550,7 +548,9 @@ describe('CheckpointProposalJob Timing Tests', () => { l1Constants, p2pPropagationTime: P2P_PROPAGATION_TIME, checkpointProposalPrepareTime: CHECKPOINT_ASSEMBLE_TIME, - enforce: true, + // 30s block duration in the 72s slot derives exactly one block, so the single built block is also + // the last block (isLastBlock=true), which is what this first-and-last-block path test asserts. + blockDurationMs: 30000, }), ); diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts index 4e07f49d831e..ed4824b62b7c 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts @@ -189,10 +189,11 @@ export class CheckpointProposalJob implements Traceable { await this.pendingL1Submission; } - /** Interrupts job-owned waits so shutdown can finish. */ + /** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */ public interrupt(): void { this.interrupted = true; this.interruptibleSleep.interrupt(true); + this.publisher.interrupt(); } private async awaitInterruptibleSleep(ms: number): Promise { @@ -907,8 +908,7 @@ export class CheckpointProposalJob implements Traceable { blockTimestamp: timestamp, // Create an empty block if we haven't already and this is the last one forceCreate: timingInfo.isLastBlock && blocksBuilt === 0 && this.config.buildCheckpointIfEmpty, - // Build deadline (absolute wall-clock seconds) is only set if we are enforcing the timetable - buildDeadline: timingInfo.deadline !== undefined ? new Date(timingInfo.deadline * 1000) : undefined, + buildDeadline: new Date(timingInfo.deadline * 1000), blockNumber, indexWithinCheckpoint, txHashesAlreadyIncluded, @@ -916,8 +916,8 @@ export class CheckpointProposalJob implements Traceable { // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot if ('failure' in buildResult) { - // If this was the last subslot, or we're running with a single block per slot, we're done - if (timingInfo.isLastBlock || timingInfo.deadline === undefined) { + // If this was the last subslot, we're done. + if (timingInfo.isLastBlock) { break; } // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again @@ -1109,6 +1109,7 @@ export class CheckpointProposalJob implements Traceable { minValidTxs, maxBlocksPerCheckpoint: this.timetable.getMaxBlocksPerCheckpoint(), perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier, + perBlockDAAllocationMultiplier: this.config.perBlockDAAllocationMultiplier, }; // Actually build the block by executing txs. The builder throws InsufficientValidTxsError @@ -1249,9 +1250,6 @@ export class CheckpointProposalJob implements Traceable { const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock; // Latest time to keep waiting for txs: wait_for_txs_deadline = block_build_deadline(k) - min_block_duration. - // buildDeadline is block_build_deadline(k) (multi-block) or the split deadline (single-block), so this - // is the uniform spec formula. Undefined if we are not enforcing the timetable (buildDeadline - // undefined), meaning we exit immediately when out of time. const startBuildingDeadline = buildDeadline ? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000) : undefined; @@ -1347,11 +1345,8 @@ export class CheckpointProposalJob implements Traceable { ); } - // Hard attestation-collection cutoff = the single consensus attestation_deadline - // (target_slot_start + S - 2E). When not enforcing the timetable, loosen to the full target slot. - const attestationDeadlineSeconds = this.config.enforceTimeTable - ? this.timetable.getAttestationDeadline(this.targetSlot) - : Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) + this.l1Constants.slotDuration; + // Hard attestation-collection cutoff = the single consensus attestation_deadline (target_slot_start + S - 2E). + const attestationDeadlineSeconds = this.timetable.getAttestationDeadline(this.targetSlot); const attestationDeadline = new Date(attestationDeadlineSeconds * 1000); this.metrics.recordRequiredAttestations( diff --git a/yarn-project/sequencer-client/src/sequencer/index.ts b/yarn-project/sequencer-client/src/sequencer/index.ts index 4db5f7152b2a..9f9721707764 100644 --- a/yarn-project/sequencer-client/src/sequencer/index.ts +++ b/yarn-project/sequencer-client/src/sequencer/index.ts @@ -1,5 +1,3 @@ -export * from './automine/automine_factory.js'; -export * from './automine/automine_sequencer.js'; export * from './checkpoint_proposal_job.js'; export * from './checkpoint_voter.js'; export * from './config.js'; diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index baa44f05b944..e60344fd8c61 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -377,9 +377,12 @@ describe('sequencer', () => { signatureContext = { chainId: chainId.toNumber(), rollupAddress: EthAddress.random() }; const config: SequencerConfig & Pick = { - enforceTimeTable: true, maxTxsPerBlock: 4, l1ChainId: signatureContext.chainId, + // With aztecSlotDuration=8 and ethereumSlotDuration=4 (fast profile), a 2s block duration derives + // exactly one valid block sub-slot. The production default (3s) would derive zero blocks for this + // slot duration and make ProposerTimetable throw on construction. + blockDurationMs: 2000, rollupAddress: signatureContext.rollupAddress, }; sequencer = new TestSequencer( @@ -676,7 +679,7 @@ describe('sequencer', () => { nowSeconds: 1000n, }); - sequencer.updateConfig({ enforceTimeTable: false, maxTxsPerBlock: 4 }); + sequencer.updateConfig({ maxTxsPerBlock: 4 }); // Build and publish 2 blocks, the sequencer should request a new publisher each time for (let i = 0; i < 2; i++) { @@ -1136,8 +1139,8 @@ describe('sequencer', () => { }); describe('modes', () => { - it('non-enforced mode', async () => { - sequencer.updateConfig({ enforceTimeTable: false, maxTxsPerBlock: 4 }); + it('builds with the default real timetable', async () => { + sequencer.updateConfig({ maxTxsPerBlock: 4 }); await setupSingleTxBlock(); @@ -1151,7 +1154,7 @@ describe('sequencer', () => { }); it('single block mode', async () => { - sequencer.updateConfig({ enforceTimeTable: true, maxTxsPerBlock: 4 }); + sequencer.updateConfig({ maxTxsPerBlock: 4 }); await setupSingleTxBlock(); @@ -1166,7 +1169,7 @@ describe('sequencer', () => { }); it('multi block mode', async () => { - sequencer.updateConfig({ enforceTimeTable: true, maxTxsPerBlock: 4, blockDurationMs: 500 }); + sequencer.updateConfig({ maxTxsPerBlock: 4, blockDurationMs: 500 }); const txs = await timesParallel(8, i => makeTx(i * 0x10000)); block = await makeBlock(txs.slice(0, 4)); @@ -1181,6 +1184,27 @@ describe('sequencer', () => { }); }); + describe('config updates', () => { + it('rejects a config with sub-minimum allocation multipliers without committing it', () => { + // Move to a 10-block geometry so the per-block allocation actually binds below the per-tx blob ceiling. + sequencer.updateConfig({ blockDurationMs: 500 }); + const goodMultiplier = sequencer.getPerBlockAllocationMultiplier(); + const goodTimetable = sequencer.getTimeTable(); + + // A sub-minimum multiplier must be rejected and must not mutate the live config or timetable. We drop + // the DA multiplier too so the DA dimension is checked against its (higher) network minimum. + expect(() => + sequencer.updateConfig({ perBlockAllocationMultiplier: 0.5, perBlockDAAllocationMultiplier: 0.5 }), + ).toThrow(/perBlockDAAllocationMultiplier \(0.5\) is below the network minimum/); + expect(sequencer.getPerBlockAllocationMultiplier()).toBe(goodMultiplier); + expect(sequencer.getTimeTable()).toBe(goodTimetable); + + // A subsequent valid update still applies, proving the rejected value never stuck. + sequencer.updateConfig({ maxTxsPerBlock: 7 }); + expect(sequencer.getPerBlockAllocationMultiplier()).toBe(goodMultiplier); + }); + }); + describe('pipelining with proposed checkpoint-based L1 check skip', () => { beforeEach(() => { // Skip execute() to avoid the pipeline sleep (which would block for 16s in real time). @@ -1608,6 +1632,10 @@ class TestSequencer extends Sequencer { return this.timetable; } + public getPerBlockAllocationMultiplier() { + return this.config.perBlockAllocationMultiplier; + } + public getLastSlotForCheckpointProposalJob() { return this.lastSlotForCheckpointProposalJob; } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 3f13990be127..cd2e66d6f4f2 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -21,6 +21,11 @@ import type { import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { ChainConfig } from '@aztec/stdlib/config'; import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; +import { + MIN_PER_BLOCK_ALLOCATION_MULTIPLIER, + MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER, + computeNetworkTxGasLimits, +} from '@aztec/stdlib/gas'; import { type ResolvedSequencerConfig, type SequencerConfig, @@ -30,13 +35,7 @@ import { import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p'; import { pickFromSchema } from '@aztec/stdlib/schemas'; -import { - DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, - DEFAULT_MIN_BLOCK_DURATION, - DEFAULT_P2P_PROPAGATION_TIME, - ProposerTimetable, -} from '@aztec/stdlib/timetable'; +import { ProposerTimetable, buildProposerTimetable } from '@aztec/stdlib/timetable'; import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client'; import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client'; @@ -151,46 +150,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter) { const filteredConfig = pickFromSchema(config, SequencerConfigSchema); + const candidate = merge(this.config, filteredConfig); + let timetable: ProposerTimetable; + try { + timetable = this.buildTimetable(candidate); + } catch (err) { + this.log.warn(`Rejecting sequencer config update: ${(err as Error).message}`, { + rejectedConfig: omit(filteredConfig, 'txPublicSetupAllowListExtend'), + }); + throw err; + } + this.config = candidate; + this.timetable = timetable; this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend')); - this.config = merge(this.config, filteredConfig); - this.timetable = this.buildTimetable(); } /** - * Builds the proposer timetable from the current config and L1 constants. The fast local/e2e profile and - * budget clamping happen inside {@link ProposerTimetable}; here we only fill the operational budgets the - * config leaves unset with the shared `DEFAULT_*` values (the config layer owns the defaults). + * Builds the proposer timetable from the given config and L1 constants via the shared + * {@link buildProposerTimetable} helper, so the sequencer derives the same blocks-per-checkpoint as the p2p + * layer and `getNodeInfo`. The fast local/e2e profile and budget clamping happen inside + * {@link ProposerTimetable}. + * + * Throws if the timing geometry is invalid or the per-block allocation multipliers are below the network + * minimums; callers must treat a throw as a rejected config and not commit it. */ - private buildTimetable(): ProposerTimetable { - const timetable = new ProposerTimetable({ - l1Constants: this.l1Constants, - blockDuration: this.config.blockDurationMs !== undefined ? this.config.blockDurationMs / 1000 : undefined, - minBlockDuration: this.config.minBlockDuration ?? DEFAULT_MIN_BLOCK_DURATION, - p2pPropagationTime: this.config.attestationPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME, - checkpointProposalPrepareTime: - this.config.checkpointProposalPrepareTime ?? DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, - checkpointProposalInitTime: DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - checkpointProposalSyncGrace: this.config.checkpointProposalSyncGraceSeconds, - enforce: this.config.enforceTimeTable, - }); + private buildTimetable(config: ResolvedSequencerConfig): ProposerTimetable { + const timetable = buildProposerTimetable(config, this.l1Constants); const maxNumberOfBlocks = timetable.getMaxBlocksPerCheckpoint(); - this.log.info( - `Sequencer timetable initialized with ${maxNumberOfBlocks} blocks per slot (${timetable.enforce ? 'enforced' : 'not enforced'})`, - { - aztecSlotDuration: timetable.aztecSlotDuration, - ethereumSlotDuration: timetable.ethereumSlotDuration, - blockDuration: timetable.blockDuration, - minBlockDuration: timetable.minBlockDuration, - p2pPropagationTime: timetable.p2pPropagationTime, - checkpointProposalPrepareTime: timetable.checkpointProposalPrepareTime, - maxNumberOfBlocks, - enforce: timetable.enforce, - }, - ); + this.log.info(`Sequencer timetable initialized with ${maxNumberOfBlocks} blocks per slot`, { + aztecSlotDuration: timetable.aztecSlotDuration, + ethereumSlotDuration: timetable.ethereumSlotDuration, + blockDuration: timetable.blockDuration, + minBlockDuration: timetable.minBlockDuration, + p2pPropagationTime: timetable.p2pPropagationTime, + checkpointProposalPrepareTime: timetable.checkpointProposalPrepareTime, + maxNumberOfBlocks, + }); if (maxNumberOfBlocks < 1) { throw new Error( @@ -199,9 +205,73 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter { - // Check we have not already processed this target slot (cheapest check) - // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not - // running against actual time (eg when we use sandbox-style automining) - if ( - this.lastSlotForCheckpointProposalJob && - this.lastSlotForCheckpointProposalJob >= targetSlot && - this.config.enforceTimeTable - ) { + // Check we have not already processed this target slot (cheapest check). + if (this.lastSlotForCheckpointProposalJob && this.lastSlotForCheckpointProposalJob >= targetSlot) { this.log.trace(`Target slot ${targetSlot} has already been processed`); return undefined; } @@ -407,7 +471,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter startDeadline) { + if (nowForStartGate > startDeadline) { this.log.debug(`Past start deadline for slot ${targetSlot}, abandoning block building`, { targetSlot, nowForStartGate, diff --git a/yarn-project/sequencer-client/src/test/utils.ts b/yarn-project/sequencer-client/src/test/utils.ts index d05d3698a47d..1eefdc0b5d22 100644 --- a/yarn-project/sequencer-client/src/test/utils.ts +++ b/yarn-project/sequencer-client/src/test/utils.ts @@ -8,6 +8,7 @@ import { Signature } from '@aztec/foundation/eth-signature'; import type { P2P } from '@aztec/p2p'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { CommitteeAttestation, L2Block } from '@aztec/stdlib/block'; +import { DEFAULT_BLOCK_DURATION_MS } from '@aztec/stdlib/config'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { BlockProposal, CheckpointAttestation, CheckpointProposal, ConsensusPayload } from '@aztec/stdlib/p2p'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; @@ -41,16 +42,14 @@ export function makeProposerTimetable(opts: { minBlockDuration?: number; p2pPropagationTime?: number; checkpointProposalPrepareTime?: number; - enforce: boolean; }): ProposerTimetable { return new ProposerTimetable({ l1Constants: opts.l1Constants, - blockDuration: opts.blockDurationMs !== undefined ? opts.blockDurationMs / 1000 : undefined, + blockDuration: (opts.blockDurationMs ?? DEFAULT_BLOCK_DURATION_MS) / 1000, minBlockDuration: opts.minBlockDuration ?? DEFAULT_MIN_BLOCK_DURATION, p2pPropagationTime: opts.p2pPropagationTime ?? DEFAULT_P2P_PROPAGATION_TIME, checkpointProposalPrepareTime: opts.checkpointProposalPrepareTime ?? DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, checkpointProposalInitTime: DEFAULT_CHECKPOINT_PROPOSAL_INIT_TIME, - enforce: opts.enforce, }); } diff --git a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index 11813d24e851..7f029d93adad 100644 --- a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -5,7 +5,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type ContractArtifact, encodeArguments } from '@aztec/stdlib/abi'; import { PublicSimulatorConfig, type PublicTxResult } from '@aztec/stdlib/avm'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { FALLBACK_TEARDOWN_DA_GAS_LIMIT, FALLBACK_TEARDOWN_L2_GAS_LIMIT, Gas, GasFees } from '@aztec/stdlib/gas'; +import { Gas, GasFees } from '@aztec/stdlib/gas'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { PublicCallRequest } from '@aztec/stdlib/kernel'; import { GlobalVariables, PublicCallRequestWithCalldata, type Tx } from '@aztec/stdlib/tx'; @@ -27,6 +27,8 @@ import { SimpleContractDataSource } from './simple_contract_data_source.js'; import { type TestPrivateInsertions, createTxForPublicCalls } from './utils.js'; const DEFAULT_GAS_FEES = new GasFees(2, 3); +const TEARDOWN_DA_GAS_LIMIT = 98_304; +const TEARDOWN_L2_GAS_LIMIT = 817_500; export type TestEnqueuedCall = { sender?: AztecAddress; @@ -132,10 +134,7 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCallRequest, feePayer, /*gasUsedByPrivate*/ teardownCall - ? new Gas( - FALLBACK_TEARDOWN_DA_GAS_LIMIT + TX_DA_GAS_OVERHEAD, - FALLBACK_TEARDOWN_L2_GAS_LIMIT + PUBLIC_TX_L2_GAS_OVERHEAD, - ) + ? new Gas(TEARDOWN_DA_GAS_LIMIT + TX_DA_GAS_OVERHEAD, TEARDOWN_L2_GAS_LIMIT + PUBLIC_TX_L2_GAS_OVERHEAD) : new Gas(TX_DA_GAS_OVERHEAD, PUBLIC_TX_L2_GAS_OVERHEAD), defaultGlobals(), gasLimits, diff --git a/yarn-project/simulator/src/public/fixtures/utils.ts b/yarn-project/simulator/src/public/fixtures/utils.ts index 2fcaf9759950..ae94a9b49f34 100644 --- a/yarn-project/simulator/src/public/fixtures/utils.ts +++ b/yarn-project/simulator/src/public/fixtures/utils.ts @@ -12,13 +12,7 @@ import { CONTRACT_INSTANCE_PUBLISHED_EVENT_TAG } from '@aztec/protocol-contracts import { bufferAsFields } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { - FALLBACK_TEARDOWN_DA_GAS_LIMIT, - FALLBACK_TEARDOWN_L2_GAS_LIMIT, - Gas, - GasFees, - GasSettings, -} from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { siloNullifier } from '@aztec/stdlib/hash'; import { LogHash, @@ -42,6 +36,9 @@ import { import { strict as assert } from 'assert'; +const TEARDOWN_DA_GAS_LIMIT = 98_304; +const TEARDOWN_L2_GAS_LIMIT = 817_500; + export type TestPrivateInsertions = { revertible?: { nullifiers?: Fr[]; @@ -132,9 +129,7 @@ export async function createTxForPublicCalls( } const maxFeesPerGas = feePayer.isZero() ? GasFees.empty() : new GasFees(10, 10); - const teardownGasLimits = teardownCallRequest - ? new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, FALLBACK_TEARDOWN_L2_GAS_LIMIT) - : Gas.empty(); + const teardownGasLimits = teardownCallRequest ? new Gas(TEARDOWN_DA_GAS_LIMIT, TEARDOWN_L2_GAS_LIMIT) : Gas.empty(); const gasSettings = new GasSettings(gasLimits, teardownGasLimits, maxFeesPerGas, GasFees.empty()); const txContext = new TxContext(Fr.zero(), Fr.zero(), gasSettings); const header = BlockHeader.empty({ globalVariables: globals }); diff --git a/yarn-project/standard-contracts/src/auth-registry/constants.ts b/yarn-project/standard-contracts/src/auth-registry/constants.ts index 3ace48fbb7b2..7f84e88f1d1e 100644 --- a/yarn-project/standard-contracts/src/auth-registry/constants.ts +++ b/yarn-project/standard-contracts/src/auth-registry/constants.ts @@ -1,8 +1,10 @@ // Address-only leaf export for browser bundles: importing from // `@aztec/standard-contracts/auth-registry/constants` avoids dragging in the // `AuthRegistry.json` static import. +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + import { StandardContractAddress, StandardContractClassId, StandardContractSalt } from '../standard_contract_data.js'; -export const STANDARD_AUTH_REGISTRY_ADDRESS = StandardContractAddress.AuthRegistry; +export const STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = StandardContractAddress.AuthRegistry; export const STANDARD_AUTH_REGISTRY_CLASS_ID = StandardContractClassId.AuthRegistry; export const STANDARD_AUTH_REGISTRY_SALT = StandardContractSalt.AuthRegistry; diff --git a/yarn-project/standard-contracts/src/handshake-registry/constants.ts b/yarn-project/standard-contracts/src/handshake-registry/constants.ts index 7f2c1c453f47..48b4cd153728 100644 --- a/yarn-project/standard-contracts/src/handshake-registry/constants.ts +++ b/yarn-project/standard-contracts/src/handshake-registry/constants.ts @@ -1,8 +1,10 @@ // Lightweight metadata leaf export for browser bundles: importing from // `@aztec/standard-contracts/handshake-registry/constants` avoids dragging in the // `HandshakeRegistry.json` static import. +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + import { StandardContractAddress, StandardContractClassId, StandardContractSalt } from '../standard_contract_data.js'; -export const STANDARD_HANDSHAKE_REGISTRY_ADDRESS = StandardContractAddress.HandshakeRegistry; +export const STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = StandardContractAddress.HandshakeRegistry; export const STANDARD_HANDSHAKE_REGISTRY_CLASS_ID = StandardContractClassId.HandshakeRegistry; export const STANDARD_HANDSHAKE_REGISTRY_SALT = StandardContractSalt.HandshakeRegistry; diff --git a/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts b/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts index f0a134722913..e7c21417a0e0 100644 --- a/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts +++ b/yarn-project/standard-contracts/src/multi-call-entrypoint/constants.ts @@ -1,8 +1,10 @@ // Lightweight metadata leaf export for browser bundles: importing from // `@aztec/standard-contracts/multi-call-entrypoint/constants` avoids dragging in the // `MultiCallEntrypoint.json` static import. +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + import { StandardContractAddress, StandardContractClassId, StandardContractSalt } from '../standard_contract_data.js'; -export const STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS = StandardContractAddress.MultiCallEntrypoint; +export const STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = StandardContractAddress.MultiCallEntrypoint; export const STANDARD_MULTI_CALL_ENTRYPOINT_CLASS_ID = StandardContractClassId.MultiCallEntrypoint; export const STANDARD_MULTI_CALL_ENTRYPOINT_SALT = StandardContractSalt.MultiCallEntrypoint; diff --git a/yarn-project/standard-contracts/src/public-checks/constants.ts b/yarn-project/standard-contracts/src/public-checks/constants.ts index 414c8e2c4e3b..7fdd07489f49 100644 --- a/yarn-project/standard-contracts/src/public-checks/constants.ts +++ b/yarn-project/standard-contracts/src/public-checks/constants.ts @@ -1,8 +1,10 @@ // Lightweight metadata leaf export for browser bundles: importing from // `@aztec/standard-contracts/public-checks/constants` avoids dragging in the // `PublicChecks.json` static import. +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; + import { StandardContractAddress, StandardContractClassId, StandardContractSalt } from '../standard_contract_data.js'; -export const STANDARD_PUBLIC_CHECKS_ADDRESS = StandardContractAddress.PublicChecks; +export const STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = StandardContractAddress.PublicChecks; export const STANDARD_PUBLIC_CHECKS_CLASS_ID = StandardContractClassId.PublicChecks; export const STANDARD_PUBLIC_CHECKS_SALT = StandardContractSalt.PublicChecks; diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 934bc65bc408..fe3a6175cef4 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -351,6 +351,13 @@ export type L2Tips = { finalized: L2TipId; }; +/** + * Tips of the L2 chain as tracked by a local provider (world-state, l2-tips-store). Omits + * `proposedCheckpoint`, which is degenerate in local stores (always equal to `checkpointed`) and + * is only meaningful on the archiver side via {@link L2BlockSource}. + */ +export type LocalL2Tips = Omit; + export const GENESIS_CHECKPOINT_HEADER_HASH = CheckpointHeader.empty().hash(); /** Identifies a block by number and hash. */ diff --git a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts index bf5831fa592f..e9e13deca97a 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts @@ -1,14 +1,38 @@ +import type { BlockNumber } from '@aztec/foundation/branded-types'; + import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { L2Block } from '../l2_block.js'; -import type { CheckpointId, L2BlockId, L2Tips } from '../l2_block_source.js'; +import type { CheckpointId, L2BlockId, L2TipId, LocalL2Tips } from '../l2_block_source.js'; /** Provides the current chain tips. Implemented by world-state, l2-tips-store, and AztecNode. */ export interface L2TipsProvider { - getL2Tips(): Promise; + getL2Tips(): Promise; } -/** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */ -export interface L2BlockStreamLocalDataProvider extends L2TipsProvider { +/** + * A block id reported by a local data provider, whose hash may be unknown when the provider cannot resolve it (e.g. + * world-state cannot resolve the hash of a proven tip ahead of its synced range). + */ +export type LocalL2BlockId = { number: BlockNumber; hash?: string }; + +/** + * Minimal local view of the chain the block stream needs to drive sync. `checkpointed` is only required when the + * stream emits checkpoint events (i.e. `ignoreCheckpoints` is off). + */ +export type LocalChainTips = { + proposed: LocalL2BlockId; + checkpointed?: { checkpoint: CheckpointId }; + proven: { block: LocalL2BlockId }; + finalized: { block: LocalL2BlockId }; +}; + +/** + * Interface to the local view of the chain. Implemented by world-state and l2-tips-store. Anything implementing + * {@link L2TipsProvider} also satisfies this contract structurally, since {@link LocalL2Tips} is assignable to + * {@link LocalChainTips}. + */ +export interface L2BlockStreamLocalDataProvider { + getL2Tips(): Promise; getL2BlockHash(number: number): Promise; } @@ -30,19 +54,27 @@ export type L2BlockStreamEvent = | /** * Reports last correct block (new tip of the proposed chain). Note that this is not necessarily the anchor block * that will be used in the transaction - if the chain has already moved past the reorg, we'll also see blocks-added - * events that will push the anchor block forward. + * events that will push the anchor block forward. `block` is the prune target (the new proposed tip); `checkpointed` + * and `proven` are the source's confirmed checkpointed and proven tips (each a block and checkpoint id). Each is used + * to clamp the corresponding local cursor when it leads the source tip, so a cursor never overshoots its own source + * frontier during a prune (the source guarantees proven <= checkpointed). */ { type: 'chain-pruned'; block: L2BlockId; - checkpoint: CheckpointId; + checkpointed: L2TipId; + proven: L2TipId; } | /** Reports new proven block. */ { type: 'chain-proven'; block: L2BlockId; + checkpoint: CheckpointId; } | /** Reports new finalized block (proven and finalized on L1). */ { type: 'chain-finalized'; block: L2BlockId; + checkpoint: CheckpointId; }; -export type L2TipsStore = L2BlockStreamEventHandler & L2BlockStreamLocalDataProvider; +export type L2TipsStore = L2BlockStreamEventHandler & + L2TipsProvider & + Pick; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index 7ab1b78ac414..1b17a151c4f5 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -1,6 +1,7 @@ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { compactArray } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; +import type { Logger } from '@aztec/foundation/log'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -16,9 +17,14 @@ import { GENESIS_CHECKPOINT_HEADER_HASH, type L2BlockId, type L2BlockSource, - type L2Tips, + type LocalL2Tips, } from '../l2_block_source.js'; -import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; +import type { + L2BlockStreamEvent, + L2BlockStreamEventHandler, + L2BlockStreamLocalDataProvider, + LocalChainTips, +} from './interfaces.js'; import { L2BlockStream } from './l2_block_stream.js'; import { L2TipsMemoryStore } from './l2_tips_memory_store.js'; @@ -242,7 +248,7 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', block: makeBlockId(36), checkpoint: makeCheckpointId(0) }, + { type: 'chain-pruned', block: makeBlockId(36), checkpointed: makeTipId(0), proven: makeTipId(0) }, { type: 'blocks-added', blocks: times(9, i => makeBlock(i + 37)) }, ] satisfies L2BlockStreamEvent[]); }); @@ -256,8 +262,8 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 41)) }, - { type: 'chain-proven', block: makeBlockId(40) }, - { type: 'chain-finalized', block: makeBlockId(35) }, + { type: 'chain-proven', block: makeBlockId(40), checkpoint: makeCheckpointId(40) }, + { type: 'chain-finalized', block: makeBlockId(35), checkpoint: makeCheckpointId(35) }, ] satisfies L2BlockStreamEvent[]); }); @@ -323,7 +329,8 @@ describe('L2BlockStream', () => { expect(handler.events[0]).toEqual({ type: 'chain-pruned', block: makeBlockId(3), - checkpoint: makeCheckpointId(3), + checkpointed: makeTipId(3), + proven: makeTipId(0), }); }); @@ -384,8 +391,8 @@ describe('L2BlockStream', () => { expectBlocksAdded([30]), expectCheckpointed(30), { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 31)) }, - { type: 'chain-proven', block: makeBlockId(25) }, - { type: 'chain-finalized', block: makeBlockId(10) }, + { type: 'chain-proven', block: makeBlockId(25), checkpoint: makeCheckpointId(25) }, + { type: 'chain-finalized', block: makeBlockId(10), checkpoint: makeCheckpointId(10) }, ]); handler.clearEvents(); @@ -393,9 +400,35 @@ describe('L2BlockStream', () => { setRemoteTips(25, 25, 25, 10); await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', block: makeBlockId(25), checkpoint: makeCheckpointId(25) }, + { type: 'chain-pruned', block: makeBlockId(25), checkpointed: makeTipId(25), proven: makeTipId(25) }, ]); }); + + // Regression test for the checkpoint-replay storm: pruning to an uncheckpointed block ahead of + // the checkpointed tip must not reset the checkpointed cursor, otherwise the next work() replays + // every checkpoint from 1 to the source tip. + it('does not replay checkpoints after pruning to an uncheckpointed block ahead of the checkpointed tip', async () => { + // Sync blocks 1-7: blocks 1-5 are checkpointed (checkpoints 1-5), blocks 6-7 uncheckpointed. + setRemoteTips(7, 5); + await blockStream.work(); + const checkpointEventsOnSync = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEventsOnSync).toHaveLength(5); + handler.clearEvents(); + + // Source drops its proposed tip to block 6 (uncheckpointed, still ahead of checkpointed=5). + // The stream prunes the local proposed tip from 7 back to 6. + setRemoteTips(6, 5); + await blockStream.work(); + expect(handler.events).toEqual([ + { type: 'chain-pruned', block: makeBlockId(6), checkpointed: makeTipId(5), proven: makeTipId(0) }, + ]); + handler.clearEvents(); + + // The next sync must NOT re-emit any chain-checkpointed events: the checkpointed cursor was + // left at block 5 / checkpoint 5, so there is nothing to replay. + await blockStream.work(); + expect(handler.events.filter(e => e.type === 'chain-checkpointed')).toEqual([]); + }); }); describe('multiple blocks per checkpoint', () => { @@ -1069,7 +1102,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(6), - checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); @@ -1109,7 +1147,7 @@ describe('L2BlockStream', () => { expectBlocksAdded([7, 8, 9]), expectCheckpointed(3), expectBlocksAdded([10, 11, 12]), - { type: 'chain-proven', block: makeBlockId(6) }, + { type: 'chain-proven', block: makeBlockId(6), checkpoint: makeCheckpointId(2) }, ]); handler.clearEvents(); @@ -1132,12 +1170,18 @@ describe('L2BlockStream', () => { await blockStream.work(); - // Should emit chain-pruned back to block 6 + // Should emit chain-pruned back to block 6, carrying the source proven tip (block 6 / ckpt 2) expect(handler.events).toEqual([ { type: 'chain-pruned', block: makeBlockId(6), - checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(6) }), + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }), }, ]); @@ -1165,7 +1209,7 @@ describe('L2BlockStream', () => { expectCheckpointed(4), expectBlocksAdded([13, 14, 15]), expectCheckpointed(5), - { type: 'chain-proven', block: makeBlockId(9) }, + { type: 'chain-proven', block: makeBlockId(9), checkpoint: makeCheckpointId(3) }, ]); }); @@ -1203,7 +1247,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(3), - checkpoint: expect.objectContaining({ number: CheckpointNumber(1) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(1) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); @@ -1258,7 +1307,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(0), - checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); @@ -1318,7 +1372,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(0), - checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); @@ -1425,7 +1484,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(6), - checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); }); @@ -1466,7 +1530,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(3), - checkpoint: expect.objectContaining({ number: CheckpointNumber(1) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(1) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); }); @@ -1482,8 +1551,8 @@ describe('L2BlockStream', () => { expectBlocksAdded([1, 2, 3]), expectBlocksAdded([4, 5, 6]), expectBlocksAdded([7, 8, 9]), - { type: 'chain-proven', block: makeBlockId(6) }, - { type: 'chain-finalized', block: makeBlockId(3) }, + { type: 'chain-proven', block: makeBlockId(6), checkpoint: makeCheckpointId(2) }, + { type: 'chain-finalized', block: makeBlockId(3), checkpoint: makeCheckpointId(1) }, ]); }); @@ -1515,8 +1584,8 @@ describe('L2BlockStream', () => { expectBlocksAdded([6]), expectBlocksAdded([7, 8, 9]), expectBlocksAdded([10, 11, 12]), - { type: 'chain-proven', block: makeBlockId(9) }, - { type: 'chain-finalized', block: makeBlockId(6) }, + { type: 'chain-proven', block: makeBlockId(9), checkpoint: makeCheckpointId(3) }, + { type: 'chain-finalized', block: makeBlockId(6), checkpoint: makeCheckpointId(2) }, ]); }); @@ -1552,7 +1621,12 @@ describe('L2BlockStream', () => { { type: 'chain-pruned', block: makeBlockId(6), - checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + checkpointed: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }), + proven: expect.objectContaining({ + block: expect.objectContaining({ number: BlockNumber(0) }), + }), }, ]); @@ -1603,8 +1677,8 @@ describe('L2BlockStream', () => { // Instead of fetching the next local block (6), we skip ahead to the latest finalized (35) and go from there. expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(6, i => makeBlock(i + 35)) }, - { type: 'chain-proven', block: makeBlockId(38) }, - { type: 'chain-finalized', block: makeBlockId(35) }, + { type: 'chain-proven', block: makeBlockId(38), checkpoint: makeCheckpointId(38) }, + { type: 'chain-finalized', block: makeBlockId(35), checkpoint: makeCheckpointId(35) }, ] satisfies L2BlockStreamEvent[]); }); @@ -1663,8 +1737,8 @@ describe('L2BlockStream', () => { expectBlocksAdded([9]), expectCheckpointed(9), { type: 'blocks-added', blocks: times(3, i => makeBlock(i + 10)) }, - { type: 'chain-proven', block: makeBlockId(9) }, - { type: 'chain-finalized', block: makeBlockId(6) }, + { type: 'chain-proven', block: makeBlockId(9), checkpoint: makeCheckpointId(9) }, + { type: 'chain-finalized', block: makeBlockId(6), checkpoint: makeCheckpointId(6) }, ]); }); @@ -1734,11 +1808,55 @@ describe('L2BlockStream', () => { }), }), { type: 'blocks-added', blocks: times(2, i => makeBlock(i + 39)) }, - { type: 'chain-proven', block: makeBlockId(38) }, - { type: 'chain-finalized', block: makeBlockId(35) }, + { type: 'chain-proven', block: makeBlockId(38), checkpoint: makeCheckpointId(38) }, + { type: 'chain-finalized', block: makeBlockId(35), checkpoint: makeCheckpointId(35) }, ]); }); }); + + describe('local provider without checkpointed tip', () => { + let localData: TestLocalChainTipsProvider; + let handler: TestL2BlockStreamEventHandler; + + beforeEach(() => { + localData = new TestLocalChainTipsProvider(); + handler = new TestL2BlockStreamEventHandler(); + }); + + it('syncs blocks with ignoreCheckpoints when no checkpointed tip is provided', async () => { + setRemoteTips(5, 5); + const blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, { + batchSize: 10, + ignoreCheckpoints: true, + }); + + await blockStream.work(); + + // All 5 blocks are synced (one per checkpoint in the mock) and no checkpoint events are emitted. + expect(handler.events).toEqual([ + expectBlocksAdded([1]), + expectBlocksAdded([2]), + expectBlocksAdded([3]), + expectBlocksAdded([4]), + expectBlocksAdded([5]), + ]); + expect(handler.events.every(e => e.type === 'blocks-added')).toBe(true); + }); + + it('surfaces a loud error when checkpoint emission is enabled without a checkpointed tip', async () => { + setRemoteTips(5, 5); + const log = mock(); + const blockStream = new TestL2BlockStream(blockSource, localData, handler, log, { batchSize: 10 }); + + await blockStream.work(); + + expect(handler.events).toEqual([]); + expect(log.error).toHaveBeenCalledWith( + `Error processing block stream`, + expect.objectContaining({ message: expect.stringContaining('does not expose a checkpointed tip') }), + ); + }); + }); }); class TestL2BlockStreamEventHandler implements L2BlockStreamEventHandler { @@ -1772,10 +1890,6 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, }; - public proposedCheckpointed = { - block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, - }; public proven = { block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, @@ -1791,11 +1905,33 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid ); } - public getL2Tips(): Promise { + public getL2Tips(): Promise { return Promise.resolve({ proposed: this.proposed, checkpointed: this.checkpointed, - proposedCheckpoint: this.proposedCheckpointed, + proven: this.proven, + finalized: this.finalized, + }); + } +} + +/** Local provider that omits `checkpointed`, mirroring world-state's `ignoreCheckpoints` configuration. */ +class TestLocalChainTipsProvider implements L2BlockStreamLocalDataProvider { + public readonly blockHashes: Record = {}; + + public proposed = { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; + public proven = { block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() } }; + public finalized = { block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() } }; + + public getL2BlockHash(number: number): Promise { + return Promise.resolve( + number > this.proposed.number ? undefined : (this.blockHashes[number] ?? new Fr(number).toString()), + ); + } + + public getL2Tips(): Promise { + return Promise.resolve({ + proposed: this.proposed, proven: this.proven, finalized: this.finalized, }); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 9a38e28d8d9c..3d5280e1684d 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -70,6 +70,13 @@ export class L2BlockStream { const localTips = await this.localData.getL2Tips(); this.log.trace(`Running L2 block stream`, { sourceTips, localTips }); + if (!this.opts.ignoreCheckpoints && localTips.checkpointed === undefined) { + throw new Error( + 'Local data provider does not expose a checkpointed tip; checkpoint events require one ' + + '(set ignoreCheckpoints or provide checkpointed tips).', + ); + } + // Check if there was a reorg and emit a chain-pruned event if so. let latestBlockNumber = localTips.proposed.number; const sourceCache = new BlockHashCache([sourceTips.proposed]); @@ -105,7 +112,8 @@ export class L2BlockStream { await this.emitEvent({ type: 'chain-pruned', block: makeL2BlockId(latestBlockNumber, hash), - checkpoint: sourceTips.checkpointed.checkpoint, + checkpointed: sourceTips.checkpointed, + proven: sourceTips.proven, }); } @@ -122,7 +130,12 @@ export class L2BlockStream { } let nextBlockNumber = latestBlockNumber + 1; - let nextCheckpointToEmit = CheckpointNumber(localTips.checkpointed.checkpoint.number + 1); + // When checkpoints are ignored the local provider may omit `checkpointed`; in that case the fallback to + // CheckpointNumber.ZERO is harmless because `nextCheckpointToEmit` is never consumed for emission (Loop 1 and + // the startingBlock/skipFinalized adjustments below only feed checkpoint emission, which is gated off). + let nextCheckpointToEmit = CheckpointNumber( + (localTips.checkpointed?.checkpoint.number ?? CheckpointNumber.ZERO) + 1, + ); // When startingBlock is set, also skip ahead for checkpoints. if ( @@ -261,10 +274,15 @@ export class L2BlockStream { await this.emitEvent({ type: 'chain-proven', block: sourceTips.proven.block, + checkpoint: sourceTips.proven.checkpoint, }); } if (localTips.finalized !== undefined && sourceTips.finalized.block.number !== localTips.finalized.block.number) { - await this.emitEvent({ type: 'chain-finalized', block: sourceTips.finalized.block }); + await this.emitEvent({ + type: 'chain-finalized', + block: sourceTips.finalized.block, + checkpoint: sourceTips.finalized.checkpoint, + }); } } catch (err: any) { if (err.name === 'AbortError') { diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index ab5f9e5f5752..08420b378077 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -1,8 +1,7 @@ -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import type { BlockNumber } from '@aztec/foundation/branded-types'; -import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { BlockHash } from '../block_hash.js'; -import type { L2BlockTag } from '../l2_block_source.js'; +import type { CheckpointId, L2BlockTag } from '../l2_block_source.js'; import { L2TipsStoreBase } from './l2_tips_store_base.js'; /** @@ -15,9 +14,8 @@ export class L2TipsMemoryStore extends L2TipsStoreBase { } private readonly tips = new Map(); + private readonly tipCheckpoints = new Map(); private readonly blockHashes = new Map(); - private readonly blockToCheckpoint = new Map(); - private readonly checkpoints = new Map(); protected getTip(tag: L2BlockTag): Promise { return Promise.resolve(this.tips.get(tag)); @@ -28,6 +26,15 @@ export class L2TipsMemoryStore extends L2TipsStoreBase { return Promise.resolve(); } + protected getTipCheckpoint(tag: L2BlockTag): Promise { + return Promise.resolve(this.tipCheckpoints.get(tag)); + } + + protected setTipCheckpoint(tag: L2BlockTag, checkpoint: CheckpointId): Promise { + this.tipCheckpoints.set(tag, checkpoint); + return Promise.resolve(); + } + protected getStoredBlockHash(blockNumber: BlockNumber): Promise { return Promise.resolve(this.blockHashes.get(blockNumber)); } @@ -46,42 +53,6 @@ export class L2TipsMemoryStore extends L2TipsStoreBase { return Promise.resolve(); } - protected getCheckpointNumberForBlock(blockNumber: BlockNumber): Promise { - return Promise.resolve(this.blockToCheckpoint.get(blockNumber)); - } - - protected setCheckpointNumberForBlock(blockNumber: BlockNumber, checkpointNumber: CheckpointNumber): Promise { - this.blockToCheckpoint.set(blockNumber, checkpointNumber); - return Promise.resolve(); - } - - protected deleteBlockToCheckpointBefore(blockNumber: BlockNumber): Promise { - for (const key of this.blockToCheckpoint.keys()) { - if (key < blockNumber) { - this.blockToCheckpoint.delete(key); - } - } - return Promise.resolve(); - } - - protected getCheckpoint(checkpointNumber: CheckpointNumber): Promise { - return Promise.resolve(this.checkpoints.get(checkpointNumber)); - } - - protected saveCheckpointData(checkpoint: PublishedCheckpoint): Promise { - this.checkpoints.set(checkpoint.checkpoint.number, checkpoint); - return Promise.resolve(); - } - - protected deleteCheckpointsBefore(checkpointNumber: CheckpointNumber): Promise { - for (const key of this.checkpoints.keys()) { - if (key < checkpointNumber) { - this.checkpoints.delete(key); - } - } - return Promise.resolve(); - } - protected runInTransaction(fn: () => Promise): Promise { // Memory store doesn't need transactions - just execute immediately return fn(); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_store_base.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_store_base.ts index 9637ff5fd17d..37a576424b29 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_store_base.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_store_base.ts @@ -1,6 +1,5 @@ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; -import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { BlockHash } from '../block_hash.js'; import type { L2Block } from '../l2_block.js'; import { @@ -8,7 +7,7 @@ import { GENESIS_CHECKPOINT_HEADER_HASH, type L2BlockId, type L2BlockTag, - type L2Tips, + type LocalL2Tips, } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; @@ -26,6 +25,12 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl /** Sets the block number for a given tag. */ protected abstract setTip(tag: L2BlockTag, blockNumber: BlockNumber): Promise; + /** Gets the checkpoint id recorded for a given tag, if any. */ + protected abstract getTipCheckpoint(tag: L2BlockTag): Promise; + + /** Records the checkpoint id for a given tag. */ + protected abstract setTipCheckpoint(tag: L2BlockTag, checkpoint: CheckpointId): Promise; + /** Gets the block hash for a given block number. */ protected abstract getStoredBlockHash(blockNumber: BlockNumber): Promise; @@ -35,27 +40,6 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl /** Deletes all block hashes for blocks before the given block number. */ protected abstract deleteBlockHashesBefore(blockNumber: BlockNumber): Promise; - /** Gets the checkpoint number for a given block number. */ - protected abstract getCheckpointNumberForBlock(blockNumber: BlockNumber): Promise; - - /** Sets the checkpoint number for a given block number. */ - protected abstract setCheckpointNumberForBlock( - blockNumber: BlockNumber, - checkpointNumber: CheckpointNumber, - ): Promise; - - /** Deletes all block-to-checkpoint mappings for blocks before the given block number. */ - protected abstract deleteBlockToCheckpointBefore(blockNumber: BlockNumber): Promise; - - /** Gets a checkpoint by its number. */ - protected abstract getCheckpoint(checkpointNumber: CheckpointNumber): Promise; - - /** Saves a checkpoint. */ - protected abstract saveCheckpointData(checkpoint: PublishedCheckpoint): Promise; - - /** Deletes all checkpoints before the given checkpoint number. */ - protected abstract deleteCheckpointsBefore(checkpointNumber: CheckpointNumber): Promise; - /** Runs the given function in a transaction. Memory stores can just execute immediately. */ protected abstract runInTransaction(fn: () => Promise): Promise; @@ -68,31 +52,26 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl return this.getStoredBlockHash(number); } - public getL2Tips(): Promise { + public getL2Tips(): Promise { return this.runInTransaction(async () => { - const [proposedBlockId, finalizedBlockId, provenBlockId, checkpointedBlockId, proposedCheckpointBlockId] = - await Promise.all([ - this.getBlockId('proposed'), - this.getBlockId('finalized'), - this.getBlockId('proven'), - this.getBlockId('checkpointed'), - this.getBlockId('proposedCheckpoint'), - ]); + const [proposedBlockId, finalizedBlockId, provenBlockId, checkpointedBlockId] = await Promise.all([ + this.getBlockId('proposed'), + this.getBlockId('finalized'), + this.getBlockId('proven'), + this.getBlockId('checkpointed'), + ]); - const [finalizedCheckpointId, provenCheckpointId, checkpointedCheckpointId, proposedCheckpointId] = - await Promise.all([ - this.getCheckpointId('finalized'), - this.getCheckpointId('proven'), - this.getCheckpointId('checkpointed'), - this.getCheckpointId('proposedCheckpoint'), - ]); + const [finalizedCheckpointId, provenCheckpointId, checkpointedCheckpointId] = await Promise.all([ + this.getCheckpointId('finalized'), + this.getCheckpointId('proven'), + this.getCheckpointId('checkpointed'), + ]); return { proposed: proposedBlockId, finalized: { block: finalizedBlockId, checkpoint: finalizedCheckpointId }, proven: { block: provenBlockId, checkpoint: provenCheckpointId }, checkpointed: { block: checkpointedBlockId, checkpoint: checkpointedCheckpointId }, - proposedCheckpoint: { block: proposedCheckpointBlockId, checkpoint: proposedCheckpointId }, }; }); } @@ -139,17 +118,21 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl private async getCheckpointId(tag: L2BlockTag): Promise { const blockNumber = await this.getTip(tag); if (blockNumber === undefined || blockNumber === 0) { - return { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }; - } - const checkpointNumber = await this.getCheckpointNumberForBlock(blockNumber); - if (checkpointNumber === undefined) { - return { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }; + return this.genesisCheckpointId(); } - const checkpoint = await this.getCheckpoint(checkpointNumber); - if (!checkpoint) { - throw new Error(`Checkpoint not found for checkpoint number ${checkpointNumber}`); + // The checkpoint id recorded for this cursor when it was last advanced is the single source of truth. + // The writers (handleChainCheckpointed/Proven/Finalized/Pruned) always record an id alongside any + // non-genesis cursor advance, so a missing id on a real block is genuine store corruption. Fail loudly + // rather than silently reporting checkpoint zero, which would drive a checkpoint-replay storm. + const storedCheckpoint = await this.getTipCheckpoint(tag); + if (storedCheckpoint !== undefined) { + return storedCheckpoint; } - return { number: checkpointNumber, hash: checkpoint.checkpoint.hash().toString() }; + throw new Error(`No checkpoint id recorded for ${tag} tip at block ${blockNumber}; the L2 tips store is corrupted`); + } + + private genesisCheckpointId(): CheckpointId { + return { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }; } private async handleBlocksAdded(event: L2BlockStreamEvent): Promise { @@ -170,14 +153,12 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl return; } await this.runInTransaction(async () => { + const checkpointId: CheckpointId = { + number: event.checkpoint.checkpoint.number, + hash: event.checkpoint.checkpoint.hash().toString(), + }; await this.saveTag('checkpointed', event.block); - await this.saveCheckpoint(event.checkpoint); - // proposedCheckpoint is always >= checkpointed. If checkpointed has caught up - // or surpassed it, advance proposedCheckpoint to match. - const proposedCheckpointBlock = await this.getBlockId('proposedCheckpoint'); - if (event.block.number > proposedCheckpointBlock.number) { - await this.saveTag('proposedCheckpoint', event.block); - } + await this.setTipCheckpoint('checkpointed', checkpointId); }); } @@ -186,12 +167,27 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl return; } await this.runInTransaction(async () => { + // A prune is a rollback: the proposed tip moves to the prune target unconditionally, but + // checkpoint-bearing cursors may only move backward. Forward-advancing them onto an + // uncheckpointed block leaves them on a block with no recorded checkpoint id, which getCheckpointId + // would then throw on. await this.saveTag('proposed', event.block); - await this.saveTag('checkpointed', event.block); - await this.saveTag('proposedCheckpoint', event.block); - const storeProven = await this.getBlockId('proven'); - if (storeProven.number > event.block.number) { - await this.saveTag('proven', event.block); + + // Clamp each checkpoint-bearing cursor down to its OWN source tip when it leads it. Clamping the proven + // cursor onto the checkpointed tip would transiently report unproven blocks as proven (the source's proven + // tip can sit below its checkpointed tip after a proof-tx reorg), until the corrective chain-proven event + // lands at the end of the same sync iteration. The event carries a valid (block, id) pair for each + // boundary, so the clamped cursor always resolves to a recorded id. The source guarantees proven <= + // checkpointed, so clamping each cursor to its own tip preserves the local proven <= checkpointed invariant. + for (const { tag, sourceTip } of [ + { tag: 'checkpointed', sourceTip: event.checkpointed }, + { tag: 'proven', sourceTip: event.proven }, + ] as const) { + const current = await this.getTip(tag); + if (current !== undefined && current > sourceTip.block.number) { + await this.saveTag(tag, sourceTip.block); + await this.setTipCheckpoint(tag, sourceTip.checkpoint); + } } }); } @@ -202,6 +198,7 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl } await this.runInTransaction(async () => { await this.saveTag('proven', event.block); + await this.setTipCheckpoint('proven', event.checkpoint); }); } @@ -211,33 +208,16 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl } await this.runInTransaction(async () => { await this.saveTag('finalized', event.block); - const finalizedCheckpointNumber = await this.getCheckpointNumberForBlock(event.block.number); + await this.setTipCheckpoint('finalized', event.checkpoint); - // Cap the deletion bound at the lowest live tip. This should always be the finalized tip, but - // we have hit bugs where this is not the case. Deleting the block hash, block-to-checkpoint mapping, - // or enclosing checkpoint object for a live tip would dangle subsequent `getBlockId`/`getCheckpointId` + // Prune block hashes below the lowest live tip. Cap the deletion bound at the lowest live tip rather + // than the finalized tip alone: this should always be the finalized tip, but we have hit bugs where + // this is not the case. Deleting the block hash for a live tip would dangle subsequent `getBlockId` // lookups and lock the block stream into an error loop. - const tips = await Promise.all([ - this.getTip('proposed'), - this.getTip('proposedCheckpoint'), - this.getTip('checkpointed'), - this.getTip('proven'), - ]); + const tips = await Promise.all([this.getTip('proposed'), this.getTip('checkpointed'), this.getTip('proven')]); const liveTipBlocks = tips.filter((t): t is BlockNumber => t !== undefined && t > 0); const safeBlockBound = BlockNumber(Math.min(event.block.number, ...liveTipBlocks)); await this.deleteBlockHashesBefore(safeBlockBound); - await this.deleteBlockToCheckpointBefore(safeBlockBound); - - if (finalizedCheckpointNumber !== undefined) { - const tipCheckpoints = await Promise.all(liveTipBlocks.map(b => this.getCheckpointNumberForBlock(b))); - const safeCheckpointBound = CheckpointNumber( - Math.min( - finalizedCheckpointNumber, - ...tipCheckpoints.filter((c): c is CheckpointNumber => c !== undefined && c > 0), - ), - ); - await this.deleteCheckpointsBefore(safeCheckpointBound); - } }); } @@ -247,14 +227,4 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl await this.setBlockHash(block.number, block.hash); } } - - private async saveCheckpoint(publishedCheckpoint: PublishedCheckpoint): Promise { - const checkpoint = publishedCheckpoint.checkpoint; - const lastBlock = checkpoint.blocks.at(-1)!; - // Only store the mapping for the last block since tips only point to checkpoint boundaries - await Promise.all([ - this.setCheckpointNumberForBlock(lastBlock.number, checkpoint.number), - this.saveCheckpointData(publishedCheckpoint), - ]); - } } diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index e721ab96d4f2..7335058c452f 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -6,6 +6,7 @@ import { GENESIS_CHECKPOINT_HEADER_HASH, L2Block, type L2BlockId, + type L2BlockTag, type L2TipId, } from '@aztec/stdlib/block'; import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; @@ -67,18 +68,11 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { checkpoint: makeCheckpointIdForBlock(blockNumber), }); - const makeTips = ( - proposed: number, - proven: number, - finalized: number, - checkpointed: number = 0, - proposedCheckpoint: number = 0, - ) => ({ + const makeTips = (proposed: number, proven: number, finalized: number, checkpointed: number = 0) => ({ proposed: makeTip(proposed), proven: makeTipId(proven), finalized: makeTipId(finalized), checkpointed: makeTipId(checkpointed), - proposedCheckpoint: makeTipId(proposedCheckpoint), }); const makeCheckpoint = async (checkpointNumber: number, blocks: L2Block[]): Promise => { @@ -153,7 +147,11 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove up to block 5 - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(5), + checkpoint: makeCheckpointIdForBlock(5), + }); const tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); @@ -173,8 +171,16 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove and finalize - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(5), + checkpoint: makeCheckpointIdForBlock(5), + }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-finalized', + block: makeBlockId(5), + checkpoint: makeCheckpointIdForBlock(5), + }); const tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); @@ -229,8 +235,16 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); // Prove and finalize up to block 3 (checkpoint 1) - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(3) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(3), + checkpoint: makeCheckpointIdForBlock(3), + }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-finalized', + block: makeBlockId(3), + checkpoint: makeCheckpointIdForBlock(3), + }); // Blocks before finalized should be cleared expect(await tipsStore.getL2BlockHash(1)).toBeUndefined(); @@ -250,7 +264,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5), - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, + checkpointed: makeTipId(0), + proven: makeTipId(0), }); const tips = await tipsStore.getL2Tips(); @@ -274,7 +289,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeTip(0), - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, + checkpointed: makeTipId(0), + proven: makeTipId(0), }); tips = await tipsStore.getL2Tips(); @@ -339,7 +355,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5), - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, + checkpointed: makeTipId(5), + proven: makeTipId(0), }); tips = await tipsStore.getL2Tips(); @@ -403,7 +420,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3), - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, + checkpointed: makeTipId(3), + proven: makeTipId(0), }); tips = await tipsStore.getL2Tips(); @@ -440,7 +458,11 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove up to block 3 - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(3), + checkpoint: makeCheckpointIdForBlock(3), + }); let tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(3)); @@ -481,7 +503,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3), - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, + checkpointed: makeTipId(3), + proven: makeTipId(3), }); tips = await tipsStore.getL2Tips(); @@ -530,15 +553,165 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(await tipsStore.getL2BlockHash(7)).not.toEqual(originalHash7); }); - // Regression test for #13142 - it('does not blow up when setting proven chain on an unseen block number', async () => { + // Regression test for #13142: proving an unseen block number (one with no local block->checkpoint + // mapping) must not blow up. With per-cursor checkpoint ids, the proven tip resolves to the + // checkpoint id carried by the event rather than falling back to checkpoint zero. + it('resolves the proven checkpoint from the carried id when setting proven on an unseen block number', async () => { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [await makeBlock(5)] }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); + // Block 3 has no local block->checkpoint mapping, but the event carries a real checkpoint id. + const carriedCheckpoint: CheckpointId = { number: CheckpointNumber(1), hash: new Fr(42).toString() }; + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(3), + checkpoint: carriedCheckpoint, + }); const tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); expect(tips.proven.block).toEqual(makeTip(3)); - // No checkpoint for block 3 since it wasn't checkpointed - expect(tips.proven.checkpoint.number).toEqual(CheckpointNumber.ZERO); + // Resolved from the carried id, not the (missing) local mapping. + expect(tips.proven.checkpoint).toEqual(carriedCheckpoint); + }); + + // proven/finalized resolve to the carried checkpoint id even when the block has no local + // block->checkpoint mapping (the cursor legitimately leads the locally-checkpointed frontier). + it('resolves proven and finalized checkpoints from carried ids without a local mapping', async () => { + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [await makeBlock(7)] }); + const provenCheckpoint: CheckpointId = { number: CheckpointNumber(2), hash: new Fr(101).toString() }; + const finalizedCheckpoint: CheckpointId = { number: CheckpointNumber(1), hash: new Fr(100).toString() }; + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(5), + checkpoint: provenCheckpoint, + }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-finalized', + block: makeBlockId(3), + checkpoint: finalizedCheckpoint, + }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proven.checkpoint).toEqual(provenCheckpoint); + expect(tips.finalized.checkpoint).toEqual(finalizedCheckpoint); + }); + + // Genuine corruption: a cursor points at a real (non-genesis) block that has a block hash but + // neither a stored per-cursor checkpoint id nor a block->checkpoint mapping. getL2Tips must throw + // loudly rather than silently report checkpoint zero. We reach this state by reaching past the + // event API to place the tip directly, since the normal writers always record an id. + it('throws when a cursor points at a real block with neither a stored id nor a mapping', async () => { + // blocks-added records the block hash for block 5 (so getBlockId succeeds) but no checkpoint id. + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [await makeBlock(5)] }); + // Corrupt the proven cursor to point at block 5 without any id or mapping. + const internal = tipsStore as unknown as { setTip(tag: L2BlockTag, blockNumber: BlockNumber): Promise }; + await internal.setTip('proven', BlockNumber(5)); + + await expect(tipsStore.getL2Tips()).rejects.toThrow(/checkpoint/i); + }); + + // Backward prune of a leading cursor: a checkpoint-bearing cursor that legitimately leads the source's + // confirmed checkpointed tip is clamped down to that tip, resolving to the id the prune event carries + // (never genesis, never a throw). This is the skipped-history shape where the cursor sits on a block + // ahead of the checkpointed frontier. + it('clamps a leading checkpoint cursor down to the source checkpointed tip carried by the prune event', async () => { + // Checkpoint blocks 1-5 (checkpointed = block 5 / ckpt 1), then add uncheckpointed blocks 6-10. + const blocks1to5 = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks1to5 }); + const checkpoint1 = await makeCheckpoint(1, blocks1to5); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); + const blocks6to10 = await Promise.all(times(5, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks6to10 }); + + // Advance the proven cursor onto an uncheckpointed block (8) via a carried id, so it LEADS the + // source checkpointed tip (block 5) and will be clamped down to it on prune. + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(8), + checkpoint: { number: CheckpointNumber(2), hash: new Fr(202).toString() }, + }); + + // Prune to block 7, carrying the source's confirmed checkpointed and proven tips (both block 5 / ckpt 1 + // here, the source's proven tip having rolled back together with its checkpointed tip). + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(7), + checkpointed: makeTipId(5), + proven: makeTipId(5), + }); + + // Must not throw; the proven cursor is clamped to the carried proven tip, resolving to ckpt 1. + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(7)); + expect(tips.proven.block).toEqual(makeTip(5)); + expect(tips.proven.checkpoint.number).toEqual(CheckpointNumber(1)); + }); + + it('keeps the checkpointed tip when pruning to an uncheckpointed block ahead of it', async () => { + const blocks1to5 = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks1to5 }); + const checkpoint1 = await makeCheckpoint(1, blocks1to5); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // checkpointed = block 5 / ckpt 1 + + const blocks6to7 = await Promise.all(times(2, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks6to7 }); // proposed = 7 + + // Prune to block 6: an uncheckpointed block AHEAD of the checkpointed tip (block 5). The source + // checkpointed tip is still block 5 / ckpt 1, so the checkpointed cursor must not move. + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(6), + checkpointed: makeTipId(5), + proven: makeTipId(0), + }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(6)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); // must NOT be zero + }); + + // Per-cursor clamping on prune: when the source rolls back its proven tip below its checkpointed tip + // (e.g. a proof tx dropped by an L1 reorg), the prune event carries both source tips and each local + // cursor must clamp to its OWN source tip. Clamping the proven cursor onto the (higher) checkpointed + // tip would transiently report unproven blocks as proven until the corrective chain-proven event lands. + it('clamps the proven cursor to the source proven tip, separately from the checkpointed cursor, on prune', async () => { + // Checkpoint blocks 1-15 across three checkpoints (ckpt 1 = 1-5, ckpt 2 = 6-10, ckpt 3 = 11-15). + const blocks1to5 = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks1to5 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(await makeCheckpoint(1, blocks1to5))); + const blocks6to10 = await Promise.all(times(5, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks6to10 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(await makeCheckpoint(2, blocks6to10))); + const blocks11to15 = await Promise.all(times(5, i => makeBlock(i + 11))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks11to15 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(await makeCheckpoint(3, blocks11to15))); + + // Prove the whole chain up to block 15 (ckpt 3), so the local proven cursor leads both source tips below. + await tipsStore.handleBlockStreamEvent({ + type: 'chain-proven', + block: makeBlockId(15), + checkpoint: makeCheckpointIdForBlock(15), + }); + + let tips = await tipsStore.getL2Tips(); + expect(tips.checkpointed.block).toEqual(makeTip(15)); + expect(tips.proven.block).toEqual(makeTip(15)); + + // Prune arrives with the source's proven tip (block 5 / ckpt 1) BELOW its checkpointed tip (block 10 / + // ckpt 2) and below the local proven cursor (block 15). Each cursor must clamp to its own source tip. + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(10), + checkpointed: makeTipId(10), + proven: makeTipId(5), + }); + + tips = await tipsStore.getL2Tips(); + // The proven cursor lands exactly on the source proven tip, NOT on the (higher) checkpointed tip. + expect(tips.proven.block).toEqual(makeTip(5)); + expect(tips.proven.checkpoint.number).toEqual(CheckpointNumber(1)); + // The checkpointed cursor lands on the source checkpointed tip. + expect(tips.checkpointed.block).toEqual(makeTip(10)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); }); } diff --git a/yarn-project/stdlib/src/config/sequencer-config.ts b/yarn-project/stdlib/src/config/sequencer-config.ts index ed0af626ed0f..f08782c1ef54 100644 --- a/yarn-project/stdlib/src/config/sequencer-config.ts +++ b/yarn-project/stdlib/src/config/sequencer-config.ts @@ -7,12 +7,16 @@ import { import type { SequencerConfig } from '../interfaces/configs.js'; import { + DEFAULT_BLOCK_DURATION, DEFAULT_CHECKPOINT_PROPOSAL_PREPARE_TIME, DEFAULT_MIN_BLOCK_DURATION, DEFAULT_P2P_PROPAGATION_TIME, getDefaultCheckpointProposalSyncGrace, } from '../timetable/index.js'; +/** Default duration per block in milliseconds, used to derive how many blocks fit in a slot. */ +export const DEFAULT_BLOCK_DURATION_MS = DEFAULT_BLOCK_DURATION * 1000; + /** Default maximum number of transactions per block. */ export const DEFAULT_MAX_TXS_PER_BLOCK = 32; @@ -40,10 +44,8 @@ export const sharedSequencerConfigMappings: ConfigMappingsType< > = { blockDurationMs: { env: 'SEQ_BLOCK_DURATION_MS', - description: - 'Duration per block in milliseconds when building multiple blocks per slot. ' + - 'If undefined (default), builds a single block per slot using the full slot duration.', - ...optionalNumberConfigHelper(), + description: 'Duration per block in milliseconds, used to derive how many blocks fit in a slot.', + ...numberConfigHelper(DEFAULT_BLOCK_DURATION_MS), }, expectedBlockProposalsPerSlot: { env: 'SEQ_EXPECTED_BLOCK_PROPOSALS_PER_SLOT', @@ -57,7 +59,7 @@ export const sharedSequencerConfigMappings: ConfigMappingsType< description: 'Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. ' + 'Defaults to twice the block duration.', - defaultValue: getDefaultCheckpointProposalSyncGrace(undefined), + defaultValue: getDefaultCheckpointProposalSyncGrace(DEFAULT_BLOCK_DURATION_MS / 1000), ...optionalNumberConfigHelper(), }, maxTxsPerBlock: { diff --git a/yarn-project/stdlib/src/contract/interfaces/node-info.ts b/yarn-project/stdlib/src/contract/interfaces/node-info.ts index 310df2e16ad0..4d779b08811d 100644 --- a/yarn-project/stdlib/src/contract/interfaces/node-info.ts +++ b/yarn-project/stdlib/src/contract/interfaces/node-info.ts @@ -5,6 +5,12 @@ import { z } from 'zod'; import { type ProtocolContractAddresses, ProtocolContractAddressesSchema } from './protocol_contract_addresses.js'; +/** Limits a single transaction may declare on a network. */ +export interface TxsLimits { + /** Maximum gas limits a single tx may declare: the smaller of the per-tx maximum and the per-block allocation. */ + gas: { daGas: number; l2Gas: number }; +} + /** Provides basic information about the running node. */ export interface NodeInfo { /** Version as tracked in the aztec-packages repository. */ @@ -21,6 +27,8 @@ export interface NodeInfo { protocolContractAddresses: ProtocolContractAddresses; /** Whether the node requires real proofs for transaction submission. */ realProofs: boolean; + /** Limits a single tx may declare on this network. Clients rely on this to set fallback gas limits. */ + txsLimits: TxsLimits; } export const NodeInfoSchema: ZodFor = z @@ -32,5 +40,8 @@ export const NodeInfoSchema: ZodFor = z l1ContractAddresses: L1ContractAddressesSchema, protocolContractAddresses: ProtocolContractAddressesSchema, realProofs: z.boolean(), + txsLimits: z.object({ + gas: z.object({ daGas: z.number().int().nonnegative(), l2Gas: z.number().int().nonnegative() }), + }), }) .transform(obj => ({ enr: undefined, ...obj })); diff --git a/yarn-project/stdlib/src/gas/README.md b/yarn-project/stdlib/src/gas/README.md index bda744329992..982252ce703b 100644 --- a/yarn-project/stdlib/src/gas/README.md +++ b/yarn-project/stdlib/src/gas/README.md @@ -146,6 +146,98 @@ newPrice = currentPrice * (10000 + modifierBps) / 10000 | `LAG` | 2 slots | | `LIFETIME` | 5 slots | +## Gas and Data Limits + +The fee model above is *how much you pay* per unit of gas; this section is *how much you may use*. Limits +form a hierarchy from a single transaction up to a whole checkpoint, and a tx that is admissible for relay +must also be buildable into a block and fit a valid checkpoint. + +### Per-tx protocol maxima + +Hard ceilings on what any single tx may declare, independent of network configuration. Declaring more is +rejected everywhere a tx is validated. + +- **`MAX_TX_DA_GAS`** (271,200) — `MAX_TX_BLOB_DATA_SIZE_IN_FIELDS` (8,475) × `DA_GAS_PER_FIELD` (32). This + is the most DA a single tx's effects can encode into a blob, so it is the most DA gas a tx could ever use. + Defined in `constants/src/constants.ts`. +- **`MAX_PROCESSABLE_L2_GAS`** (6,540,000) — the AVM's maximum processable L2 gas, derived in Noir as + `PUBLIC_TX_L2_GAS_OVERHEAD + AVM_MAX_PROCESSABLE_L2_GAS` (`constants/src/constants.gen.ts`). + +### Network admission limits + +The most a single tx may *declare* and still be relayed across the network. Computed by +`computeNetworkTxGasLimits` in `tx_gas_limits.ts` per dimension as: + +``` +min(per-tx max, ceil(checkpointBudget / blocksPerCheckpoint * minMultiplier)) +``` + +The per-block share mirrors what a proposer grants the first block of a checkpoint +(`CheckpointBuilder.capLimitsByCheckpointBudgets`), so a tx declaring this much is packable into a block. +The network-minimum multipliers are `MIN_PER_BLOCK_ALLOCATION_MULTIPLIER` (1.2, L2 and tx count) and +`MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER` (1.5, DA). DA's is higher so a maximal contract class +registration (~97k DA gas) fits a single block at mainnet geometry (72s slots, 6s blocks → 10 blocks per +checkpoint). + +The DA budget is `getDaCheckpointBudgetForTxs(maxBlocksPerCheckpoint)`, not the raw +`MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT` (786,432). Blob encoding spends overhead fields that no tx pays DA +gas for — one checkpoint-end marker field and the per-block block-end fields (7 for the first block, 6 for +each subsequent block, `blob-lib/src/encoding/block_blob_data.ts`) — so the raw constant is unattainable. The +getter nets out the full overhead for a checkpoint of `maxBlocksPerCheckpoint` blocks: at mainnet geometry +(10 blocks) that is `(24,576 − 1 − 7 − 9×6) × 32 = 24,514 × 32 = 784,448` DA gas. Subtracting every block's +overhead (not just the first) keeps admission at or below the builder's first-block blob-field cap at every +geometry — the builder is the most generous for the first block (it only reserves that block's own block-end +overhead), so being conservative here is what guarantees admitted ⇒ buildable. Without this netting a tx +near the raw limit would be admitted but never buildable. + +These limits depend on network-wide inputs only (timetable-derived blocks-per-checkpoint, checkpoint +budgets, the network-minimum multipliers), never on a node's local restrictiveness. Every node always +advertises them in `NodeInfo.txsLimits` (a required field); wallets read it and pass `txsLimits.gas` to +`GasSettings.fallback` as the default gas limits when sending without explicit limits, and they are enforced +by `GasLimitsValidator` (clamped to the per-tx protocol maxima) at three points: RPC tx acceptance +(`aztec-node/src/aztec-node/server.ts`), gossip validation (`p2p/src/services/libp2p/libp2p_service.ts`), +and pending-pool admission (`p2p/src/client/factory.ts`). They are deliberately *not* enforced at reqresp or +block-proposal validation — admission is relay policy, not block validity. + +### Per-block builder budgets + +While packing a checkpoint, `CheckpointBuilder.capLimitsByCheckpointBudgets` +(`validator-client/src/checkpoint_builder.ts`) computes each block's budget as a fair share of the remaining +checkpoint budget across the remaining blocks, scaled by the configured multipliers. Operators may raise the +multipliers above the network minimums but not lower them — the sequencer fails startup otherwise +(`assertConfigMeetsNetworkTxLimits` in `sequencer-client/src/sequencer/sequencer.ts`), since a node that +allocates less than it admits would accept txs over RPC/gossip that its builder can never pack. + +The fair share is then min'ed with the operator's absolute per-block caps `maxL2BlockGas` / `maxDABlockGas` +and the blob-field cap (checkpoint capacity net of the checkpoint-end marker and this block's block-end +overhead). The absolute caps are allowed to be restrictive: a cap below the network admission limit only +produces a startup warning, not a failure, because such txs simply stay in the pool for other proposers to +include. + +### Per-checkpoint budgets + +The outermost limits, enforced as proposal validity in `validateCheckpointLimits` +(`stdlib/src/checkpoint/validate.ts`) and physically by blob encoding: + +- **Mana** — total L2 gas across all blocks ≤ `rollupManaLimit` (= `manaTarget × 2` on L1, + `l1-contracts/src/core/libraries/rollup/FeeLib.sol`). +- **DA gas** — total DA gas ≤ raw `MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT` (786,432). +- **Blob fields** — total ≤ `BLOBS_PER_CHECKPOINT × FIELDS_PER_BLOB` (6 × 4,096 = 24,576). +- **Tx counts** — total txs ≤ `maxTxsPerCheckpoint` when configured. + +### Summary + +| Limit | Value (mainnet defaults) | Scope | Where enforced | +| --------------------------------------- | ------------------------------- | ------------- | ----------------------------------------------------------- | +| `MAX_TX_DA_GAS` | 271,200 | per-tx | every gas validator (hard ceiling) | +| `MAX_PROCESSABLE_L2_GAS` | 6,540,000 | per-tx | every gas validator (hard ceiling) | +| Network DA admission limit | min(271,200, ceil(784,448/10×1.5)) = 117,668 | per-tx (relay) | RPC, gossip, pending pool (`GasLimitsValidator`) | +| Network L2 admission limit | min(6,540,000, ceil(manaLimit/10×1.2)) | per-tx (relay) | RPC, gossip, pending pool (`GasLimitsValidator`) | +| Per-block fair share + caps | remaining budget / blocks × multiplier, min absolute caps & blob-field cap | per-block | `CheckpointBuilder.capLimitsByCheckpointBudgets` | +| `rollupManaLimit` | `manaTarget × 2` | per-checkpoint | `validateCheckpointLimits` | +| `MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT` | 786,432 | per-checkpoint | `validateCheckpointLimits` + blob encoding | +| `BLOBS_PER_CHECKPOINT × FIELDS_PER_BLOB`| 24,576 | per-checkpoint | `validateCheckpointLimits` + blob encoding | + ## TypeScript Types - **`Gas`** — mana quantity in two dimensions (`daGas`, `l2Gas`). diff --git a/yarn-project/stdlib/src/gas/gas_settings.test.ts b/yarn-project/stdlib/src/gas/gas_settings.test.ts new file mode 100644 index 000000000000..8782e34d1605 --- /dev/null +++ b/yarn-project/stdlib/src/gas/gas_settings.test.ts @@ -0,0 +1,31 @@ +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; + +import { Gas } from './gas.js'; +import { GasFees } from './gas_fees.js'; +import { GasSettings } from './gas_settings.js'; + +describe('GasSettings.fallback', () => { + const maxFeesPerGas = new GasFees(10, 10); + + it('uses the gas limits supplied by the caller', () => { + const gasLimits = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); + const settings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); + expect(settings.gasLimits.daGas).toBe(gasLimits.daGas); + expect(settings.gasLimits.l2Gas).toBe(gasLimits.l2Gas); + }); + + it('keeps default teardown limits at or below the total limits', () => { + const gasLimits = new Gas(MAX_TX_DA_GAS, MAX_PROCESSABLE_L2_GAS); + const settings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); + expect(settings.teardownGasLimits.daGas).toBeLessThanOrEqual(settings.gasLimits.daGas); + expect(settings.teardownGasLimits.l2Gas).toBeLessThanOrEqual(settings.gasLimits.l2Gas); + }); + + it('derives teardown from explicit gas limits so teardown never exceeds the total', () => { + // A small total (e.g. a network with many blocks per checkpoint) must still produce a valid teardown. + const gasLimits = new Gas(100, 800); + const settings = GasSettings.fallback({ gasLimits, maxFeesPerGas }); + expect(settings.teardownGasLimits.daGas).toBeLessThanOrEqual(gasLimits.daGas); + expect(settings.teardownGasLimits.l2Gas).toBeLessThanOrEqual(gasLimits.l2Gas); + }); +}); diff --git a/yarn-project/stdlib/src/gas/gas_settings.ts b/yarn-project/stdlib/src/gas/gas_settings.ts index f1b472ef69f0..4672f16be52a 100644 --- a/yarn-project/stdlib/src/gas/gas_settings.ts +++ b/yarn-project/stdlib/src/gas/gas_settings.ts @@ -8,13 +8,6 @@ import { z } from 'zod'; import { Gas, GasDimensions } from './gas.js'; import { GasFees } from './gas_fees.js'; -/** Approximate max DA gas limit. Arbitrary, assuming 4 blocks per checkpoint — users should use gas estimation. */ -export const APPROXIMATE_MAX_DA_GAS_PER_BLOCK = Math.floor(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT / 4); -/** Fallback teardown L2 gas limit. Arbitrary — users should use gas estimation. */ -export const FALLBACK_TEARDOWN_L2_GAS_LIMIT = Math.floor(MAX_PROCESSABLE_L2_GAS / 8); -/** Fallback teardown DA gas limit. Arbitrary — users should use gas estimation. */ -export const FALLBACK_TEARDOWN_DA_GAS_LIMIT = Math.floor(APPROXIMATE_MAX_DA_GAS_PER_BLOCK / 2); - // For gas estimation, we use intentionally high limits above what the network can process, // so the simulation runs without hitting gas caps. Since teardown gas is counted towards total, // the total estimation limit is teardown + max processable. @@ -106,31 +99,28 @@ export class GasSettings { /** * Fills in gas limits high enough for transactions to be included in most cases. - * gasLimits is set to the maximum the protocol allows; since teardown gas is reserved - * from gasLimits during private execution (see gas_meter.nr), the effective gas available - * for app logic will be gasLimits - teardownGasLimits - privateOverhead. - * The DA gas limit is set to an approximate max per block assuming 4 blocks per checkpoint, - * since using the maximum per checkpoint would cause nodes to reject transactions. + * Callers must supply `gasLimits` — typically the most a single tx may declare on the network + * (`min(per-tx max, per-block allocation)`), i.e. a node's advertised `txsLimits.gas`. Since teardown gas + * is reserved from gasLimits during private execution (see gas_meter.nr), the effective gas available for + * app logic is gasLimits - teardownGasLimits - privateOverhead; the teardown default is derived from the + * effective total so it always stays below it. * These values won't work if: * - Teardown consumes more than the arbitrarily assigned fallback limits * - The rest of the transaction consumes more than the remaining gas after teardown * - The DA gas limit is too low for the transaction, while still within the checkpoint limit */ static fallback(overrides: { - gasLimits?: Gas; + gasLimits: Gas; teardownGasLimits?: Gas; maxFeesPerGas: GasFees; maxPriorityFeesPerGas?: GasFees; }) { + const gasLimits = overrides.gasLimits; + const teardownGasLimits = + overrides.teardownGasLimits ?? new Gas(Math.floor(gasLimits.daGas / 2), Math.floor(gasLimits.l2Gas / 8)); return GasSettings.from({ - gasLimits: overrides.gasLimits ?? { - l2Gas: MAX_PROCESSABLE_L2_GAS, - daGas: APPROXIMATE_MAX_DA_GAS_PER_BLOCK, - }, - teardownGasLimits: overrides.teardownGasLimits ?? { - l2Gas: FALLBACK_TEARDOWN_L2_GAS_LIMIT, - daGas: FALLBACK_TEARDOWN_DA_GAS_LIMIT, - }, + gasLimits, + teardownGasLimits, maxFeesPerGas: overrides.maxFeesPerGas, maxPriorityFeesPerGas: overrides.maxPriorityFeesPerGas ?? GasFees.empty(), }); diff --git a/yarn-project/stdlib/src/gas/index.ts b/yarn-project/stdlib/src/gas/index.ts index 0e75b6a99c9f..20025e941405 100644 --- a/yarn-project/stdlib/src/gas/index.ts +++ b/yarn-project/stdlib/src/gas/index.ts @@ -3,3 +3,4 @@ export * from './gas.js'; export * from './gas_fees.js'; export * from './gas_settings.js'; export * from './gas_used.js'; +export * from './tx_gas_limits.js'; diff --git a/yarn-project/stdlib/src/gas/tx_gas_limits.test.ts b/yarn-project/stdlib/src/gas/tx_gas_limits.test.ts new file mode 100644 index 000000000000..a6621bf28fef --- /dev/null +++ b/yarn-project/stdlib/src/gas/tx_gas_limits.test.ts @@ -0,0 +1,130 @@ +import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding'; +import { + BLOBS_PER_CHECKPOINT, + CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, + DA_GAS_PER_FIELD, + FIELDS_PER_BLOB, + MAX_PROCESSABLE_L2_GAS, + MAX_TX_DA_GAS, + TX_DA_GAS_OVERHEAD, +} from '@aztec/constants'; + +import { buildProposerTimetable } from '../timetable/build_proposer_timetable.js'; +import { + MIN_PER_BLOCK_ALLOCATION_MULTIPLIER, + MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER, + computeNetworkTxGasLimits, + getDaCheckpointBudgetForTxs, + getNetworkTxGasLimits, +} from './tx_gas_limits.js'; + +const MANA_CHECKPOINT_BUDGET = 10_000_000; + +describe('computeNetworkTxGasLimits', () => { + it('caps DA gas at the per-block allocation when it is below the per-tx blob ceiling', () => { + const gas = computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: 10, manaCheckpointBudget: MANA_CHECKPOINT_BUDGET }); + expect(gas.daGas).toBe(Math.ceil((getDaCheckpointBudgetForTxs(10) / 10) * MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER)); + expect(gas.daGas).toBeLessThan(MAX_TX_DA_GAS); + }); + + it('admitted tx always fits the first-block blob-field cap across all valid geometries', () => { + // Guards against the mismatch where the admission DA limit uses the raw checkpoint capacity but the + // builder's blob-field cap uses the overhead-adjusted capacity, causing txs to be admitted but never + // buildable at certain blocks-per-checkpoint geometries. + for (let b = 1; b <= 24; b++) { + const admittedBlobFields = Math.floor( + computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: b, manaCheckpointBudget: MANA_CHECKPOINT_BUDGET }).daGas / + DA_GAS_PER_FIELD, + ); + const firstBlockBlobFieldCap = Math.ceil( + ((BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS - getNumBlockEndBlobFields(true)) / + b) * + MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER, + ); + expect(admittedBlobFields).toBeLessThanOrEqual(firstBlockBlobFieldCap); + } + }); + + it('caps DA gas at the per-tx blob ceiling in single-block mode', () => { + // With a single block the even share is the full checkpoint budget, which exceeds what one tx can post. + const gas = computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: 1, manaCheckpointBudget: MANA_CHECKPOINT_BUDGET }); + expect(gas.daGas).toBe(MAX_TX_DA_GAS); + }); + + it('caps L2 gas at the per-block mana allocation when a budget is given', () => { + const manaCheckpointBudget = 10_000_000; + const gas = computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: 10, manaCheckpointBudget }); + expect(gas.l2Gas).toBe( + Math.min(MAX_PROCESSABLE_L2_GAS, Math.ceil((manaCheckpointBudget / 10) * MIN_PER_BLOCK_ALLOCATION_MULTIPLIER)), + ); + }); + + it('clamps L2 gas by the checkpoint mana budget at blocks=1 (multiplier would overshoot)', () => { + // At a single block the per-block share is the whole budget, and the >1 multiplier would push the limit + // above the budget itself — admitting a tx no builder can ever pack (the builder caps L2 by remainingMana). + // The budget clamp keeps the advertised L2 limit at or below the budget. + const manaCheckpointBudget = 1_000_000; + const gas = computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: 1, manaCheckpointBudget }); + expect(gas.l2Gas).toBeLessThanOrEqual(manaCheckpointBudget); + }); +}); + +describe('getDaCheckpointBudgetForTxs', () => { + it('clamps to zero for absurd geometries instead of going negative', () => { + expect(getDaCheckpointBudgetForTxs(10_000)).toBe(0); + expect( + computeNetworkTxGasLimits({ maxBlocksPerCheckpoint: 10_000, manaCheckpointBudget: MANA_CHECKPOINT_BUDGET }).daGas, + ).toBeGreaterThanOrEqual(0); + }); +}); + +describe('getNetworkTxGasLimits', () => { + const l1Constants = { + l1GenesisTime: 0n, + slotDuration: 72, + ethereumSlotDuration: 12, + rollupManaLimit: 10_000_000, + }; + + it('derives the limit from config + L1 constants using the network-minimum multipliers', () => { + const gas = getNetworkTxGasLimits({ blockDurationMs: 6000 }, l1Constants); + const maxBlocksPerCheckpoint = buildProposerTimetable( + { blockDurationMs: 6000 }, + l1Constants, + ).getMaxBlocksPerCheckpoint(); + const expected = computeNetworkTxGasLimits({ + maxBlocksPerCheckpoint, + manaCheckpointBudget: l1Constants.rollupManaLimit, + }); + expect(gas.daGas).toBe(expected.daGas); + expect(gas.l2Gas).toBe(expected.l2Gas); + }); +}); + +describe('v5 mainnet geometry (72s slots / 6s blocks → 10 blocks per checkpoint)', () => { + // Largest tx we want to support: a maximal contract class registration, dominated by its contract class + // log (content + contract-address field) plus the fixed tx overhead. Deploy-side nullifiers add a handful + // more fields, so this is a lower bound on the true largest deploy. + const largestDeployDaGas = (CONTRACT_CLASS_LOG_SIZE_IN_FIELDS + 1) * DA_GAS_PER_FIELD + TX_DA_GAS_OVERHEAD; + const maxBlocksPerCheckpoint = 10; + + it('the timetable derives 10 blocks per checkpoint', () => { + const blocks = buildProposerTimetable( + { blockDurationMs: 6000 }, + { l1GenesisTime: 0n, slotDuration: 72, ethereumSlotDuration: 12 }, + ).getMaxBlocksPerCheckpoint(); + expect(blocks).toBe(maxBlocksPerCheckpoint); + }); + + it('fits the largest contract class deploy with the DA multiplier, but not with the general multiplier', () => { + // Green: the 1.5 DA multiplier (used by computeNetworkTxGasLimits) leaves room for the largest deploy. + expect( + computeNetworkTxGasLimits({ maxBlocksPerCheckpoint, manaCheckpointBudget: MANA_CHECKPOINT_BUDGET }).daGas, + ).toBeGreaterThanOrEqual(largestDeployDaGas); + + // Red: the general 1.2 multiplier would not — the higher DA-specific minimum is what fits the deploy. + const daBudget = getDaCheckpointBudgetForTxs(maxBlocksPerCheckpoint); + const generalMultiplierDaGas = Math.ceil((daBudget / maxBlocksPerCheckpoint) * MIN_PER_BLOCK_ALLOCATION_MULTIPLIER); + expect(generalMultiplierDaGas).toBeLessThan(largestDeployDaGas); + }); +}); diff --git a/yarn-project/stdlib/src/gas/tx_gas_limits.ts b/yarn-project/stdlib/src/gas/tx_gas_limits.ts new file mode 100644 index 000000000000..17c00bfb8548 --- /dev/null +++ b/yarn-project/stdlib/src/gas/tx_gas_limits.ts @@ -0,0 +1,123 @@ +import { + NUM_BLOCK_END_BLOB_FIELDS, + NUM_CHECKPOINT_END_MARKER_FIELDS, + NUM_FIRST_BLOCK_END_BLOB_FIELDS, +} from '@aztec/blob-lib/encoding'; +import { + BLOBS_PER_CHECKPOINT, + DA_GAS_PER_FIELD, + FIELDS_PER_BLOB, + MAX_PROCESSABLE_L2_GAS, + MAX_TX_DA_GAS, +} from '@aztec/constants'; + +import { type ProposerTimetableConfig, buildProposerTimetable } from '../timetable/build_proposer_timetable.js'; +import type { SlotTimingConstants } from '../timetable/consensus_timetable.js'; +import { Gas } from './gas.js'; + +/** + * Network-minimum per-block budget multiplier for L2 gas and tx-count allocation. A block packer must + * grant at least this share of the even per-block split to a single tx; operators may configure a higher + * multiplier (more generous), but not a lower one — enforced at sequencer startup. Also used as the default + * for `SequencerConfig.perBlockAllocationMultiplier`. + */ +export const MIN_PER_BLOCK_ALLOCATION_MULTIPLIER = 1.2; + +/** + * Network-minimum per-block budget multiplier for DA gas, applied in place of the general + * {@link MIN_PER_BLOCK_ALLOCATION_MULTIPLIER}. Higher than the general multiplier so the largest tx we + * want to support — a maximal contract class registration (~97k DA gas) — fits a single block under v5 + * mainnet geometry (72s slots, 6s blocks → 10 blocks per checkpoint). A builder may configure a higher + * multiplier but never a lower one. Also used as the default for + * `SequencerConfig.perBlockDAAllocationMultiplier`. + */ +export const MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER = 1.5; + +/** + * The DA gas budget available to tx data within a checkpoint of `maxBlocksPerCheckpoint` blocks. This is the + * raw blob capacity (`BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB * DA_GAS_PER_FIELD`) minus the fields the blob + * encoding reserves for overhead that no tx pays DA gas for: + * + * - one checkpoint-end marker field (`NUM_CHECKPOINT_END_MARKER_FIELDS`), + * - the first block's block-end fields (`NUM_FIRST_BLOCK_END_BLOB_FIELDS`, 7), and + * - `NUM_BLOCK_END_BLOB_FIELDS` (6) for each of the `blocks - 1` subsequent blocks. + * + * Subtracting the overhead for every block (not just the first) keeps the network DA admission limit at or + * below the builder's first-block blob-field cap at every geometry. The builder is the MOST generous for the + * first block — it only reserves that block's own block-end overhead — so being conservative here (assuming + * the checkpoint is full of blocks, each spending its share) is what guarantees admitted ⇒ buildable: a tx + * admitted under this budget always fits the first block's blob-field cap, regardless of how many blocks the + * builder ends up packing. + * + * @param maxBlocksPerCheckpoint - Number of blocks the checkpoint may contain; clamped to at least 1. + */ +export function getDaCheckpointBudgetForTxs(maxBlocksPerCheckpoint: number): number { + const blocks = Math.max(1, maxBlocksPerCheckpoint); + // Clamp at zero: for absurd geometries (blocks greater than ~4094) the per-block overhead alone exceeds the + // raw blob capacity, which would otherwise yield a negative advertised DA budget. + const fields = Math.max( + 0, + BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - + NUM_CHECKPOINT_END_MARKER_FIELDS - + NUM_FIRST_BLOCK_END_BLOB_FIELDS - + (blocks - 1) * NUM_BLOCK_END_BLOB_FIELDS, + ); + return fields * DA_GAS_PER_FIELD; +} + +/** + * Computes the maximum gas a single tx may declare on a network: the smaller of the per-tx protocol + * maximum and the per-block allocation a proposer grants to the first block of a checkpoint. The per-block + * allocation mirrors `CheckpointBuilder.capLimitsByCheckpointBudgets` + * (`ceil(checkpointBudget / maxBlocksPerCheckpoint * multiplier)`) using the network-minimum multipliers, so + * a tx declaring this much is admissible into a block under that geometry. + * + * This is a *network* limit: a function of network-wide constants only (timetable-derived + * blocks-per-checkpoint, checkpoint budgets, the network-minimum multipliers). It must NOT depend on a + * node's local restrictiveness — its multipliers configured above the network minimum, or its + * `maxDABlockGas` / `validateMaxDABlockGas` caps — because those make a node stricter at block-building + * time but cannot define what the network considers a valid tx for relay. The same value is advertised by + * `getNodeInfo` and enforced by the RPC/gossip/pool gas validators. + * + * The DA budget is {@link getDaCheckpointBudgetForTxs} evaluated at the clamped blocks-per-checkpoint — the + * raw blob capacity net of encoding overhead for every block — so the admission limit is consistent with the + * builder's blob-field cap. + * + * @param manaCheckpointBudget - L2 (mana) budget per checkpoint (`rollupManaLimit`). + */ +export function computeNetworkTxGasLimits(opts: { maxBlocksPerCheckpoint: number; manaCheckpointBudget: number }): Gas { + const blocks = Math.max(1, opts.maxBlocksPerCheckpoint); + const daBudget = getDaCheckpointBudgetForTxs(blocks); + + // Clamp by the whole-checkpoint budget too: at small block counts the per-block share scaled by the + // multiplier can exceed the checkpoint budget itself (e.g. at blocks=1 a >1 multiplier overshoots), which + // would admit a tx no builder can ever pack — the builder caps each block by the remaining budget. Clamping + // by the budget makes "admitted ⇒ buildable" unconditional. (For DA the per-tx maximum always binds first, + // so the budget clamp is currently moot, but it keeps the invariant explicit.) + const daGas = Math.min( + MAX_TX_DA_GAS, + daBudget, + Math.ceil((daBudget / blocks) * MIN_PER_BLOCK_DA_ALLOCATION_MULTIPLIER), + ); + const l2Gas = Math.min( + MAX_PROCESSABLE_L2_GAS, + opts.manaCheckpointBudget, + Math.ceil((opts.manaCheckpointBudget / blocks) * MIN_PER_BLOCK_ALLOCATION_MULTIPLIER), + ); + + return new Gas(daGas, l2Gas); +} + +/** + * Network tx gas limits derived from a sequencer/p2p config and the L1 slot-timing + mana constants. The + * single source of truth shared by `getNodeInfo` (advertising) and the RPC/gossip/pool gas validators + * (enforcing), so a node never rejects a tx it advertised as admissible. Always uses the network-minimum + * multipliers, never the node's (possibly higher) configured multipliers. + */ +export function getNetworkTxGasLimits( + config: ProposerTimetableConfig, + l1Constants: SlotTimingConstants & { rollupManaLimit: number }, +): Gas { + const maxBlocksPerCheckpoint = buildProposerTimetable(config, l1Constants).getMaxBlocksPerCheckpoint(); + return computeNetworkTxGasLimits({ maxBlocksPerCheckpoint, manaCheckpointBudget: l1Constants.rollupManaLimit }); +} diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts index 89b3d50fa901..14cb9deff36c 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.test.ts @@ -138,6 +138,7 @@ class MockAztecNodeAdmin implements AztecNodeAdmin { disableValidator: false, disabledValidators: [], attestationPollingIntervalMs: 1000, + blockDurationMs: 3000, disableTransactions: false, haSigningEnabled: false, nodeId: 'test-node-id', diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts index 66ce9d00ad76..546c0c625aa6 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-admin.ts @@ -133,7 +133,7 @@ export function createAztecNodeAdminClient( apiKey?: string, ): AztecNodeAdmin { return createSafeJsonRpcClient(url, AztecNodeAdminApiSchema, { - namespaceMethods: 'nodeAdmin', + namespaceMethods: 'aztecAdmin', fetch, onResponse: getVersioningResponseHandler(versions), ...(apiKey ? { extraHeaders: { 'x-api-key': apiKey } } : {}), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts index f84b8a253103..2355115f8340 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-debug.test.ts @@ -1,3 +1,4 @@ +import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; import { type AztecNodeDebug, AztecNodeDebugApiSchema } from './aztec-node-debug.js'; @@ -26,10 +27,19 @@ describe('AztecNodeDebugApiSchema', () => { it('mineBlock', async () => { await context.client.mineBlock(); }); + + it('prove', async () => { + expect(await context.client.prove()).toEqual(7); + expect(await context.client.prove(CheckpointNumber(3))).toEqual(3); + }); }); class MockAztecNodeDebug implements AztecNodeDebug { mineBlock(): Promise { return Promise.resolve(); } + + prove(upToCheckpoint?: CheckpointNumber): Promise { + return Promise.resolve(upToCheckpoint ?? CheckpointNumber(7)); + } } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts b/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts index 77b06c29a5b7..ccaec0f4093c 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node-debug.ts @@ -1,8 +1,9 @@ +import { type CheckpointNumber, CheckpointNumberSchema } from '@aztec/foundation/branded-types'; import { createSafeJsonRpcClient, defaultFetch } from '@aztec/foundation/json-rpc/client'; import { z } from 'zod'; -import type { ApiSchemaFor } from '../schemas/schemas.js'; +import { type ApiSchemaFor, optional } from '../schemas/schemas.js'; import { type ComponentsVersions, getVersioningResponseHandler } from '../versioning/index.js'; /** @@ -19,10 +20,24 @@ export interface AztecNodeDebug { * @throws If no sequencer is running. */ mineBlock(): Promise; + + /** + * Synthetically proves the L2 chain up to the given checkpoint (default: the latest checkpointed + * checkpoint), writing epoch out hashes into the L1 Outbox so L2-to-L1 messages become consumable + * and advancing the rollup's proven tip. There is no real proof — this is the local-network + * equivalent of an epoch proof landing on L1. The target is clamped to the latest checkpointed + * checkpoint and the call no-ops when it is already proven. + * + * @param upToCheckpoint - Checkpoint to prove up to; defaults to the latest checkpointed checkpoint. + * @returns The proven checkpoint number after the call. + * @throws If no automine sequencer is running (only the automine sequencer supports synthetic proving). + */ + prove(upToCheckpoint?: CheckpointNumber): Promise; } export const AztecNodeDebugApiSchema: ApiSchemaFor = { mineBlock: z.function({ input: z.tuple([]), output: z.void() }), + prove: z.function({ input: z.tuple([optional(CheckpointNumberSchema)]), output: CheckpointNumberSchema }), }; export function createAztecNodeDebugClient( @@ -32,7 +47,7 @@ export function createAztecNodeDebugClient( apiKey?: string, ): AztecNodeDebug { return createSafeJsonRpcClient(url, AztecNodeDebugApiSchema, { - namespaceMethods: 'nodeDebug', + namespaceMethods: 'aztecDebug', fetch, onResponse: getVersioningResponseHandler(versions), ...(apiKey ? { extraHeaders: { 'x-api-key': apiKey } } : {}), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index ce21a3f8c5e1..bd404091f483 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -1,6 +1,12 @@ import { ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; import { type L1ContractAddresses, L1ContractsNames } from '@aztec/ethereum/l1-contract-addresses'; -import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { + BlockNumber, + CheckpointNumber, + CheckpointProposalHash, + EpochNumber, + SlotNumber, +} from '@aztec/foundation/branded-types'; import { randomInt } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; import { memoize } from '@aztec/foundation/decorators'; @@ -26,12 +32,14 @@ import { ProtocolContractsNames, getContractClassFromArtifact, } from '../contract/index.js'; +import { EmptyL1RollupConstants, type L1RollupConstants } from '../epoch-helpers/index.js'; import { GasFees } from '../gas/gas_fees.js'; import { PublicKeys } from '../keys/public_keys.js'; import { type LogResult, randomLogResult } from '../logs/log_result.js'; import type { PrivateLogsQuery, PublicLogsQuery } from '../logs/logs_query.js'; import { SiloedTag } from '../logs/siloed_tag.js'; import { Tag } from '../logs/tag.js'; +import { CheckpointAttestation } from '../p2p/checkpoint_attestation.js'; import { getTokenContractArtifact } from '../tests/fixtures.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; import { NullifierMembershipWitness } from '../trees/nullifier_membership_witness.js'; @@ -60,6 +68,7 @@ import type { ChainTip, ChainTips } from './chain_tips.js'; import type { CheckpointParameter } from './checkpoint_parameter.js'; import type { CheckpointIncludeOptions, CheckpointResponse } from './checkpoint_response.js'; import type { SequencerConfig } from './configs.js'; +import type { PeerInfo, ProposalsForSlot } from './p2p.js'; import type { ProverConfig } from './prover-client.js'; import type { WorldStateSyncStatus } from './world_state.js'; @@ -103,6 +112,31 @@ describe('AztecNodeApiSchema', () => { }); }); + it('getL1Constants', async () => { + const result = await context.client.getL1Constants(); + expect(result).toEqual(EmptyL1RollupConstants); + }); + + it('getSyncedL2SlotNumber', async () => { + const result = await context.client.getSyncedL2SlotNumber(); + expect(result).toEqual(SlotNumber(1)); + }); + + it('getSyncedL2EpochNumber', async () => { + const result = await context.client.getSyncedL2EpochNumber(); + expect(result).toEqual(EpochNumber(1)); + }); + + it('getSyncedL1Timestamp', async () => { + const result = await context.client.getSyncedL1Timestamp(); + expect(result).toEqual(1n); + }); + + it('getProposalsForSlot', async () => { + const result = await context.client.getProposalsForSlot(SlotNumber(1)); + expect(result).toEqual({ blockProposals: [], checkpointProposals: [] }); + }); + it('findLeavesIndexes', async () => { const response = await context.client.findLeavesIndexes(BlockNumber(1), MerkleTreeId.ARCHIVE, [ Fr.random(), @@ -186,7 +220,7 @@ describe('AztecNodeApiSchema', () => { it('getPredictedMinFees', async () => { const response = await context.client.getPredictedMinFees(); - expect(response).toEqual([GasFees.empty()]); + expect(response).toEqual([new GasFees(1, 2), new GasFees(3, 4)]); }); it('getMaxPriorityFees', async () => { @@ -526,6 +560,19 @@ describe('AztecNodeApiSchema', () => { expect(response).toEqual([]); }); + it('getPeers', async () => { + const response = await context.client.getPeers(); + expect(response).toEqual([{ status: 'connected', score: 1, id: 'peer-id' }]); + }); + + it('getCheckpointAttestationsForSlot', async () => { + const response = await context.client.getCheckpointAttestationsForSlot( + SlotNumber(1), + CheckpointProposalHash('0xdeadbeef'), + ); + expect(response[0]).toBeInstanceOf(CheckpointAttestation); + }); + it('getWorldStateSyncStatus', async () => { const response = await context.client.getWorldStateSyncStatus(); expect(response).toEqual(await handler.getWorldStateSyncStatus()); @@ -562,6 +609,22 @@ class MockAztecNode implements AztecNode { }); } + getL1Constants(): Promise { + return Promise.resolve(EmptyL1RollupConstants); + } + + getSyncedL2SlotNumber(): Promise { + return Promise.resolve(SlotNumber(1)); + } + + getSyncedL2EpochNumber(): Promise { + return Promise.resolve(EpochNumber(1)); + } + + getSyncedL1Timestamp(): Promise { + return Promise.resolve(1n); + } + getBlock( _param: BlockParameter, _options?: Opts, @@ -697,7 +760,7 @@ class MockAztecNode implements AztecNode { return Promise.resolve(GasFees.empty()); } getPredictedMinFees(): Promise { - return Promise.resolve([GasFees.empty()]); + return Promise.resolve([new GasFees(1, 2), new GasFees(3, 4)]); } getMaxPriorityFees(): Promise { return Promise.resolve(GasFees.empty()); @@ -725,6 +788,7 @@ class MockAztecNode implements AztecNode { ) as L1ContractAddresses, protocolContractAddresses: Object.fromEntries(protocolContracts) as ProtocolContractAddresses, realProofs: true, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, }; } getNodeVersion(): Promise { @@ -880,4 +944,22 @@ class MockAztecNode implements AztecNode { getAllowedPublicSetup(): Promise { return Promise.resolve([]); } + getPeers(_includePending?: boolean): Promise { + return Promise.resolve([{ status: 'connected', score: 1, id: 'peer-id' }]); + } + getCheckpointAttestationsForSlot( + slot: SlotNumber, + proposalPayloadHash?: CheckpointProposalHash, + ): Promise { + expect(typeof slot).toBe('number'); + if (proposalPayloadHash !== undefined) { + expect(typeof proposalPayloadHash).toBe('string'); + } + return Promise.resolve([CheckpointAttestation.empty()]); + } + + getProposalsForSlot(slot: SlotNumber): Promise { + expect(typeof slot).toBe('number'); + return Promise.resolve({ blockProposals: [], checkpointProposals: [] }); + } } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index f026ebaaff94..99ee4d88a88c 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -7,9 +7,11 @@ import { type CheckpointNumber, CheckpointNumberPositiveSchema, CheckpointNumberSchema, + type CheckpointProposalHash, type EpochNumber, EpochNumberSchema, type SlotNumber, + SlotNumberSchema, } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { EthAddress } from '@aztec/foundation/eth-address'; @@ -35,6 +37,7 @@ import { type ProtocolContractAddresses, ProtocolContractAddressesSchema, } from '../contract/index.js'; +import { type L1RollupConstants, L1RollupConstantsSchema } from '../epoch-helpers/index.js'; import { ManaUsageEstimate } from '../gas/fee_math.js'; import { GasFees } from '../gas/gas_fees.js'; import { type LogResult, LogResultSchema } from '../logs/log_result.js'; @@ -45,7 +48,8 @@ import { PublicLogsQuerySchema, } from '../logs/logs_query.js'; import { type L2ToL1MembershipWitness, L2ToL1MembershipWitnessSchema } from '../messaging/l2_to_l1_membership.js'; -import { type ApiSchemaFor, type ZodFor, optional, schemas } from '../schemas/schemas.js'; +import { CheckpointAttestation } from '../p2p/checkpoint_attestation.js'; +import { type ApiSchemaFor, optional, schemas } from '../schemas/schemas.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; import { NullifierMembershipWitness } from '../trees/nullifier_membership_witness.js'; import { PublicDataWitness } from '../trees/public_data_witness.js'; @@ -84,18 +88,12 @@ import { type CheckpointResponse, CheckpointResponseSchema, } from './checkpoint_response.js'; +import { type GetTxByHashOptions, GetTxByHashOptionsSchema } from './get_tx_by_hash_options.js'; +import { type PeerInfo, PeerInfoSchema, type ProposalsForSlot, ProposalsForSlotSchema } from './p2p.js'; import { type WorldStateSyncStatus, WorldStateSyncStatusSchema } from './world_state.js'; -/** Options for retrieving txs via {@link AztecNode.getTxByHash} and {@link AztecNode.getTxsByHash}. */ -export type GetTxByHashOptions = { - /** Keep the proof on the returned tx; stripped by default. */ - includeProof?: boolean; -}; - -/** Zod schema for {@link GetTxByHashOptions}. */ -export const GetTxByHashOptionsSchema: ZodFor = z.object({ - includeProof: z.boolean().optional(), -}); +export type { GetTxByHashOptions } from './get_tx_by_hash_options.js'; +export { GetTxByHashOptionsSchema } from './get_tx_by_hash_options.js'; /** * The aztec node. @@ -252,6 +250,22 @@ export interface AztecNode { /** Returns the tips of the L2 chain. */ getChainTips(): Promise; + /** Returns the rollup constants for the current chain. */ + getL1Constants(): Promise; + + /** + * Returns the last L2 slot number for which the node has all L1 data needed to build the next checkpoint. + */ + getSyncedL2SlotNumber(): Promise; + + /** + * Returns the last L2 epoch number that has been fully synchronized from L1. + */ + getSyncedL2EpochNumber(): Promise; + + /** Returns the latest L1 timestamp according to the archiver's synced L1 view. */ + getSyncedL1Timestamp(): Promise; + /** * Gets lightweight checkpoint metadata for a contiguous range or for an entire epoch. * @param query - Either `{ from, limit }` or `{ epoch }`. @@ -329,7 +343,7 @@ export interface AztecNode { * Each entry accounts for the L1 gas oracle transition and congestion growth based on the * given mana usage estimate. Defaults to target usage (steady state). * @param manaUsage - Expected mana usage per checkpoint (none, target, or limit). - * @returns An array of GasFees, one per slot in the prediction window. + * @returns An array of GasFees with current min fees first, followed by one entry per predicted slot. */ getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise; @@ -526,6 +540,30 @@ export interface AztecNode { * @returns The list of allowed elements. */ getAllowedPublicSetup(): Promise; + + /** + * Returns info for all connected, dialing, and cached peers. Only available when P2P is enabled. + * @param includePending - If true, also include peers in the pending state. + */ + getPeers(includePending?: boolean): Promise; + + /** + * Queries the attestation pool for checkpoint attestations for the given slot. + * @param slot - The slot to query. + * @param proposalPayloadHash - Hex-encoded keccak256 of the target proposal's signed payload hash. + * When provided, only attestations whose payload hash matches are returned. + * When omitted, all attestations for the slot are returned. + */ + getCheckpointAttestationsForSlot( + slot: SlotNumber, + proposalPayloadHash?: CheckpointProposalHash, + ): Promise; + + /** + * Returns block and checkpoint proposals retained in the attestation pool for the given slot. + * Only available when P2P is enabled. + */ + getProposalsForSlot(slot: SlotNumber): Promise; } const MAX_SIGNATURES_PER_REGISTER_CALL = 100; @@ -590,6 +628,14 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getChainTips: z.function({ input: z.tuple([]), output: ChainTipsSchema }), + getL1Constants: z.function({ input: z.tuple([]), output: L1RollupConstantsSchema }), + + getSyncedL2SlotNumber: z.function({ input: z.tuple([]), output: SlotNumberSchema.optional() }), + + getSyncedL2EpochNumber: z.function({ input: z.tuple([]), output: EpochNumberSchema.optional() }), + + getSyncedL1Timestamp: z.function({ input: z.tuple([]), output: schemas.BigInt.optional() }), + getCheckpointsData: z.function({ input: z.tuple([CheckpointsQuerySchema]), output: z.array(CheckpointDataSchema) }), getBlock: z.function({ @@ -730,6 +776,21 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getEncodedEnr: z.function({ input: z.tuple([]), output: z.string().optional() }), getAllowedPublicSetup: z.function({ input: z.tuple([]), output: z.array(AllowedElementSchema) }), + + getPeers: z.function({ input: z.tuple([optional(z.boolean())]), output: z.array(PeerInfoSchema) }), + + getCheckpointAttestationsForSlot: z.function({ + input: z.tuple([ + schemas.SlotNumber, + optional(z.string().regex(/^0x[0-9a-fA-F]+$/) as unknown as z.ZodType), + ]), + output: z.array(CheckpointAttestation.schema), + }), + + getProposalsForSlot: z.function({ + input: z.tuple([schemas.SlotNumber]), + output: ProposalsForSlotSchema, + }), }; export function createAztecNodeClient( @@ -739,7 +800,7 @@ export function createAztecNodeClient( batchWindowMS = 0, ): AztecNode { return createSafeJsonRpcClient(url, AztecNodeApiSchema, { - namespaceMethods: 'node', + namespaceMethods: 'aztec', fetch, batchWindowMS, onResponse: getVersioningResponseHandler(versions), diff --git a/yarn-project/stdlib/src/interfaces/block-builder.ts b/yarn-project/stdlib/src/interfaces/block-builder.ts index 8e09e36c1320..aa08afc85559 100644 --- a/yarn-project/stdlib/src/interfaces/block-builder.ts +++ b/yarn-project/stdlib/src/interfaces/block-builder.ts @@ -64,6 +64,8 @@ type ProposerBlockBuilderOptions = BlockBuilderOptionsBase & { maxBlocksPerCheckpoint: number; /** Per-block gas budget multiplier. Budget = (remaining / remainingBlocks) * multiplier. */ perBlockAllocationMultiplier: number; + /** Per-block budget multiplier for DA gas and blob fields. Falls back to perBlockAllocationMultiplier when unset. */ + perBlockDAAllocationMultiplier?: number; }; /** Validator mode: no redistribution params needed. */ diff --git a/yarn-project/stdlib/src/interfaces/client.ts b/yarn-project/stdlib/src/interfaces/client.ts index be668152d079..52f51521c187 100644 --- a/yarn-project/stdlib/src/interfaces/client.ts +++ b/yarn-project/stdlib/src/interfaces/client.ts @@ -1,6 +1,7 @@ export * from './aztec-node.js'; export * from './aztec-node-admin.js'; export * from './aztec-node-debug.js'; +export { type PeerInfo, type ProposalsForSlot } from './p2p.js'; export * from './block_response.js'; export * from './chain_tips.js'; export * from './checkpoint_parameter.js'; diff --git a/yarn-project/stdlib/src/interfaces/configs.ts b/yarn-project/stdlib/src/interfaces/configs.ts index 0b32bc1ca134..a806a31e0354 100644 --- a/yarn-project/stdlib/src/interfaces/configs.ts +++ b/yarn-project/stdlib/src/interfaces/configs.ts @@ -30,6 +30,11 @@ export interface SequencerConfig { maxDABlockGas?: number; /** Per-block gas budget multiplier for both L2 and DA gas. Budget = (checkpointLimit / maxBlocks) * multiplier. */ perBlockAllocationMultiplier?: number; + /** + * Per-block budget multiplier applied to DA gas and blob fields in place of `perBlockAllocationMultiplier`. + * Defaults higher than the general multiplier so the largest contract class deploy fits a single block. + */ + perBlockDAAllocationMultiplier?: number; /** Redistribute remaining checkpoint budget evenly across remaining blocks instead of allowing a single block to consume the entire remaining budget. */ redistributeCheckpointBudget?: boolean; /** Recipient of block reward. */ @@ -44,8 +49,6 @@ export interface SequencerConfig { txPublicSetupAllowListExtend?: AllowedElement[]; /** Payload address to vote for */ governanceProposerPayload?: EthAddress; - /** Whether to enforce the time table when building blocks */ - enforceTimeTable?: boolean; /** * Minimum block-building time (`min_block_duration`) still worth allocating if the proposer starts * late, in seconds. @@ -56,6 +59,8 @@ export interface SequencerConfig { * checkpoint proposal being ready for p2p send, in seconds. */ checkpointProposalPrepareTime?: number; + /** How much time (in seconds) we allow in the slot for publishing the L1 tx. */ + l1PublishingTime?: number; /** Used for testing to introduce a fake delay after processing each tx */ fakeProcessingDelayPerTxMs?: number; /** Used for testing to throw an error after processing N txs */ @@ -95,7 +100,7 @@ export interface SequencerConfig { fishermanMode?: boolean; /** Shuffle attestation ordering to create invalid ordering (for testing only) */ shuffleAttestationOrdering?: boolean; - /** Duration per block in milliseconds when building multiple blocks per slot (default: undefined = single block per slot) */ + /** Duration per block in milliseconds, used to derive how many blocks fit in a slot (defaults to 3000 ms). */ blockDurationMs?: number; /** Consensus grace in seconds for a received checkpoint proposal to materialize into local proposed state. */ checkpointProposalSyncGraceSeconds?: number; @@ -135,6 +140,7 @@ export const SequencerConfigSchema = zodFor()( publishTxsWithProposals: z.boolean().optional(), maxDABlockGas: z.number().optional(), perBlockAllocationMultiplier: z.number().optional(), + perBlockDAAllocationMultiplier: z.number().optional(), redistributeCheckpointBudget: z.boolean().optional(), coinbase: schemas.EthAddress.optional(), feeRecipient: schemas.AztecAddress.optional(), @@ -144,7 +150,7 @@ export const SequencerConfigSchema = zodFor()( governanceProposerPayload: schemas.EthAddress.optional(), minBlockDuration: z.number().positive().optional(), checkpointProposalPrepareTime: z.number().nonnegative().optional(), - enforceTimeTable: z.boolean().optional(), + l1PublishingTime: z.number().optional(), fakeProcessingDelayPerTxMs: z.number().optional(), fakeThrowAfterProcessingTxCount: z.number().optional(), attestationPropagationTime: z.number().optional(), @@ -176,8 +182,6 @@ export const SequencerConfigSchema = zodFor()( type SequencerConfigOptionalKeys = | 'governanceProposerPayload' - | 'blockDurationMs' - | 'checkpointProposalSyncGraceSeconds' | 'expectedBlockProposalsPerSlot' | 'coinbase' | 'feeRecipient' diff --git a/yarn-project/stdlib/src/interfaces/get_tx_by_hash_options.ts b/yarn-project/stdlib/src/interfaces/get_tx_by_hash_options.ts new file mode 100644 index 000000000000..bad8c7397f85 --- /dev/null +++ b/yarn-project/stdlib/src/interfaces/get_tx_by_hash_options.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +import type { ZodFor } from '../schemas/schemas.js'; + +/** Options for retrieving txs via {@link AztecNode.getTxByHash} and {@link AztecNode.getTxsByHash}. */ +export type GetTxByHashOptions = { + /** Keep the proof on the returned tx; stripped by default. */ + includeProof?: boolean; +}; + +/** Zod schema for {@link GetTxByHashOptions}. */ +export const GetTxByHashOptionsSchema: ZodFor = z.object({ + includeProof: z.boolean().optional(), +}); diff --git a/yarn-project/stdlib/src/interfaces/p2p.ts b/yarn-project/stdlib/src/interfaces/p2p.ts index 3ae83155005c..1090c59c3ae4 100644 --- a/yarn-project/stdlib/src/interfaces/p2p.ts +++ b/yarn-project/stdlib/src/interfaces/p2p.ts @@ -1,22 +1,23 @@ import type { CheckpointProposalHash, SlotNumber } from '@aztec/foundation/branded-types'; +import { bufferSchemaFor } from '@aztec/foundation/schemas'; import { z } from 'zod'; -import type { BlockProposal } from '../p2p/block_proposal.js'; +import { BlockProposal } from '../p2p/block_proposal.js'; import { CheckpointAttestation } from '../p2p/checkpoint_attestation.js'; -import type { CheckpointProposalCore } from '../p2p/checkpoint_proposal.js'; +import { CheckpointProposal, type CheckpointProposalCore } from '../p2p/checkpoint_proposal.js'; import { type ApiSchemaFor, optional, schemas } from '../schemas/index.js'; import { Tx } from '../tx/tx.js'; import { TxHash } from '../tx/tx_hash.js'; import { MAX_RPC_TXS_LEN } from './api_limit.js'; -import { type GetTxByHashOptions, GetTxByHashOptionsSchema } from './aztec-node.js'; +import { type GetTxByHashOptions, GetTxByHashOptionsSchema } from './get_tx_by_hash_options.js'; export type PeerInfo = | { status: 'connected'; score: number; id: string } | { status: 'dialing'; dialStatus: string; id: string; addresses: string[] } | { status: 'cached'; id: string; addresses: string[]; enr: string; dialAttempts: number }; -const PeerInfoSchema = z.discriminatedUnion('status', [ +export const PeerInfoSchema = z.discriminatedUnion('status', [ z.object({ status: z.literal('connected'), score: z.number(), id: z.string() }), z.object({ status: z.literal('dialing'), dialStatus: z.string(), id: z.string(), addresses: z.array(z.string()) }), z.object({ @@ -69,20 +70,32 @@ export interface P2PApi { ): Promise; } +export type ProposalsForSlot = { + blockProposals: BlockProposal[]; + checkpointProposals: CheckpointProposalCore[]; +}; + export interface P2PClient extends P2PApi { /** Manually adds checkpoint attestations to the p2p client attestation pool. */ addOwnCheckpointAttestations(attestations: CheckpointAttestation[]): Promise; /** Returns retained signed proposals for a slot. */ - getProposalsForSlot(slot: SlotNumber): Promise<{ - blockProposals: BlockProposal[]; - checkpointProposals: CheckpointProposalCore[]; - }>; + getProposalsForSlot(slot: SlotNumber): Promise; /** Returns whether a checkpoint proposal was retained for a slot. */ hasCheckpointProposalForSlot(slot: SlotNumber): Promise; } +const MAX_PROPOSALS_FOR_SLOT_RPC_LEN = 256; + +export const BlockProposalSchema = bufferSchemaFor(BlockProposal); +export const CheckpointProposalSchema = bufferSchemaFor(CheckpointProposal); + +export const ProposalsForSlotSchema = z.object({ + blockProposals: z.array(BlockProposalSchema).max(MAX_PROPOSALS_FOR_SLOT_RPC_LEN), + checkpointProposals: z.array(CheckpointProposalSchema).max(MAX_PROPOSALS_FOR_SLOT_RPC_LEN), +}); + export const P2PApiSchema: ApiSchemaFor = { getCheckpointAttestationsForSlot: z.function({ input: z.tuple([ diff --git a/yarn-project/stdlib/src/interfaces/validator.ts b/yarn-project/stdlib/src/interfaces/validator.ts index 9455ace39edf..f17e96e2a475 100644 --- a/yarn-project/stdlib/src/interfaces/validator.ts +++ b/yarn-project/stdlib/src/interfaces/validator.ts @@ -83,10 +83,10 @@ export type ValidatorClientConfig = ValidatorHASignerConfig & }; export type ValidatorClientFullConfig = ValidatorClientConfig & - Pick< - SequencerConfig, - 'txPublicSetupAllowListExtend' | 'broadcastInvalidBlockProposal' | 'maxBlocksPerCheckpoint' | 'blockDurationMs' - > & + Pick & + // `blockDurationMs` is optional on the loose `SequencerConfig` but is always populated via the shared + // `numberConfigHelper(3000)` mapping, so it is required on the fully-resolved validator config. + Required> & Pick< SlasherConfig, | 'slashBroadcastedInvalidBlockPenalty' @@ -132,8 +132,8 @@ export const ValidatorClientFullConfigSchema = zodFor { expect(timetable.getAttestationDeadline(slot)).toBe(targetSlotStart + 48); }); - it('does not require a block duration for the attestation deadline', () => { - const single = new ConsensusTimetable({ l1Constants: l1Constants(S, E), blockDuration: undefined }); - expect(single.getAttestationDeadline(slot)).toBe(targetSlotStart + 48); - }); - - it('drops the D term from the checkpoint proposal receive deadline in single-block mode', () => { - const single = new ConsensusTimetable({ l1Constants: l1Constants(S, E), blockDuration: undefined }); - expect(() => single.getCheckpointProposalReceiveDeadline(slot)).not.toThrow(); - expect(single.getCheckpointProposalReceiveDeadline(slot)).toBe(targetSlotStart - E); - }); - it('handles slot 0 without throwing (p2p validators evaluate windows for peer-supplied slots)', () => { const zero = SlotNumber.ZERO; expect(() => timetable.getBuildFrameStart(zero)).not.toThrow(); diff --git a/yarn-project/stdlib/src/timetable/consensus_timetable.ts b/yarn-project/stdlib/src/timetable/consensus_timetable.ts index 67369639f026..e500fc300f52 100644 --- a/yarn-project/stdlib/src/timetable/consensus_timetable.ts +++ b/yarn-project/stdlib/src/timetable/consensus_timetable.ts @@ -24,8 +24,8 @@ export class ConsensusTimetable { /** Ethereum slot duration (`E`) in seconds. */ public readonly ethereumSlotDuration: number; - /** Block sub-slot duration (`D`) in seconds, or undefined in single-block mode. */ - public readonly blockDuration: number | undefined; + /** Block sub-slot duration (`D`) in seconds. */ + public readonly blockDuration: number; /** L1 genesis timestamp in seconds (`genesis`), the anchor all slot timings derive from. */ public readonly genesisTime: bigint; @@ -33,11 +33,7 @@ export class ConsensusTimetable { /** Consensus grace for received checkpoint proposals to materialize into local proposed state. */ public readonly checkpointProposalSyncGrace: number; - constructor(opts: { - l1Constants: SlotTimingConstants; - blockDuration: number | undefined; - checkpointProposalSyncGrace?: number; - }) { + constructor(opts: { l1Constants: SlotTimingConstants; blockDuration: number; checkpointProposalSyncGrace?: number }) { const { l1Constants, blockDuration } = opts; const checkpointProposalSyncGrace = opts.checkpointProposalSyncGrace ?? getDefaultCheckpointProposalSyncGrace(blockDuration); @@ -47,8 +43,8 @@ export class ConsensusTimetable { if (l1Constants.ethereumSlotDuration <= 0) { throw new Error(`ethereumSlotDuration must be positive (got ${l1Constants.ethereumSlotDuration})`); } - if (blockDuration !== undefined && blockDuration <= 0) { - throw new Error(`blockDuration must be positive when provided (got ${blockDuration})`); + if (blockDuration <= 0) { + throw new Error(`blockDuration must be positive (got ${blockDuration})`); } if (checkpointProposalSyncGrace < 0) { throw new Error(`checkpointProposalSyncGrace must be non-negative (got ${checkpointProposalSyncGrace})`); @@ -85,12 +81,10 @@ export class ConsensusTimetable { /** * Hard consensus receive deadline for a checkpoint proposal: `target_slot_start - E - D`. Validators - * reject proposals arriving after this, and the next proposer does not build on them. In single-block - * mode (`blockDuration` undefined) the `D` term drops to zero, giving `target_slot_start - E` (the - * next proposer's build-frame boundary), so this remains usable rather than throwing. + * reject proposals arriving after this, and the next proposer does not build on them. */ public getCheckpointProposalReceiveDeadline(slot: SlotNumber): number { - return this.getTargetSlotStart(slot) - this.ethereumSlotDuration - (this.blockDuration ?? 0); + return this.getTargetSlotStart(slot) - this.ethereumSlotDuration - this.blockDuration; } /** @@ -100,7 +94,7 @@ export class ConsensusTimetable { */ public getCheckpointProposalSyncedDeadline(slot: SlotNumber): number { return Math.ceil( - this.getCheckpointProposalReceiveDeadline(slot) + (this.blockDuration ?? 0) + this.checkpointProposalSyncGrace, + this.getCheckpointProposalReceiveDeadline(slot) + this.blockDuration + this.checkpointProposalSyncGrace, ); } diff --git a/yarn-project/stdlib/src/timetable/index.ts b/yarn-project/stdlib/src/timetable/index.ts index 476384e394d6..3c0332601c04 100644 --- a/yarn-project/stdlib/src/timetable/index.ts +++ b/yarn-project/stdlib/src/timetable/index.ts @@ -1,3 +1,4 @@ export * from './budgets.js'; +export * from './build_proposer_timetable.js'; export * from './consensus_timetable.js'; export * from './proposer_timetable.js'; diff --git a/yarn-project/stdlib/src/timetable/proposer_timetable.test.ts b/yarn-project/stdlib/src/timetable/proposer_timetable.test.ts index 83254b6da11d..0687232409ab 100644 --- a/yarn-project/stdlib/src/timetable/proposer_timetable.test.ts +++ b/yarn-project/stdlib/src/timetable/proposer_timetable.test.ts @@ -49,7 +49,6 @@ describe('ProposerTimetable', () => { minBlockDuration: 2, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); it('derives max_blocks = 10', () => { @@ -92,7 +91,6 @@ describe('ProposerTimetable', () => { minBlockDuration: 1, p2pPropagationTime: 0.5, checkpointProposalPrepareTime: 0.5, - enforce: true, }); const slot = SlotNumber(3); const targetSlotStart = 36 * slot; @@ -117,7 +115,6 @@ describe('ProposerTimetable', () => { minBlockDuration: 1, p2pPropagationTime: 0.5, checkpointProposalPrepareTime: 0.5, - enforce: true, }); it('derives max_blocks = 3', () => { @@ -139,7 +136,6 @@ describe('ProposerTimetable', () => { minBlockDuration: 2, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); it('selects the first sub-slot at the build frame start', () => { @@ -192,7 +188,6 @@ describe('ProposerTimetable', () => { minBlockDuration: 2, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); it('clamps budgets to the fast profile', () => { @@ -226,70 +221,6 @@ describe('ProposerTimetable', () => { }); }); }); - - describe('non-enforced mode', () => { - const timetable = makeProposerTimetable({ - l1Constants: l1Constants(72, 12), - blockDuration: 6, - enforce: false, - }); - - it('always allows starting a single last block with no deadline', () => { - const result = timetable.selectNextSubslot(SlotNumber(5), Number.MAX_SAFE_INTEGER); - expect(result.canStart).toBe(true); - expect(result.isLastBlock).toBe(true); - expect(result.deadline).toBeUndefined(); - }); - }); - - describe('single-block enforced mode (no blockDuration)', () => { - const S = 72; - const E = 12; - const slot = SlotNumber(5); - const targetSlotStart = S * slot; - - const timetable = makeProposerTimetable({ - l1Constants: l1Constants(S, E), - blockDuration: undefined, - minBlockDuration: 2, - enforce: true, - }); - - it('reports a single block per checkpoint', () => { - expect(timetable.getMaxBlocksPerCheckpoint()).toBe(1); - }); - - it('splits the remaining time until the attestation deadline between execution and re-execution', () => { - const now = targetSlotStart - 20; - const attestationDeadline = targetSlotStart + 48; - const available = (attestationDeadline - now) / 2; - const result = timetable.selectNextSubslot(slot, now); - expect(result.canStart).toBe(true); - expect(result.isLastBlock).toBe(true); - expect(result.deadline).toBe(now + available); - }); - - it('refuses to start when the split time falls below minD', () => { - const attestationDeadline = targetSlotStart + 48; - const now = attestationDeadline - 2 * 2 + 0.1; // less than 2*minD remaining - const result = timetable.selectNextSubslot(slot, now); - expect(result.canStart).toBe(false); - }); - - it('keeps the start deadline at attestation_deadline - 2*minD (matching selectNextSubslot)', () => { - const attestationDeadline = targetSlotStart + 48; - expect(timetable.getBuildStartDeadline(slot)).toBe(attestationDeadline - 2 * 2); - }); - - it('never abandons a slot that selectNextSubslot would still allow to start', () => { - // The latest now at which the single-block branch still allows a start: now <= attestationDeadline - // - 2*minD. The build-entry gate must not give up before then, so getBuildStartDeadline must be >= it. - const startDeadline = timetable.getBuildStartDeadline(slot); - expect(timetable.selectNextSubslot(slot, startDeadline).canStart).toBe(true); - // Just past the start deadline both must agree the slot is gone. - expect(timetable.selectNextSubslot(slot, startDeadline + 0.001).canStart).toBe(false); - }); - }); }); describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { @@ -299,7 +230,6 @@ describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { blockDuration: 6, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(10); }); @@ -310,7 +240,6 @@ describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { blockDuration: 6, p2pPropagationTime: 0.5, checkpointProposalPrepareTime: 0.5, - enforce: true, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(4); }); @@ -321,16 +250,14 @@ describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { blockDuration: 8, p2pPropagationTime: 0.5, checkpointProposalPrepareTime: 0.5, - enforce: true, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(3); }); - it('returns 1 for single-block mode', () => { + it('can derive one block per checkpoint with a concrete block duration', () => { const timetable = makeProposerTimetable({ l1Constants: l1Constants(72, 12), - blockDuration: undefined, - enforce: true, + blockDuration: 24, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(1); }); @@ -344,7 +271,6 @@ describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { blockDuration: 8, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(3); }); @@ -356,7 +282,6 @@ describe('ProposerTimetable.getMaxBlocksPerCheckpoint', () => { blockDuration: 6, p2pPropagationTime: 2, checkpointProposalPrepareTime: 1, - enforce: true, }); expect(timetable.getMaxBlocksPerCheckpoint()).toBe(4); }); @@ -421,7 +346,6 @@ describe('e2e multi-block-per-checkpoint capacity', () => { const timetable = makeProposerTimetable({ l1Constants: l1Constants(S, E), blockDuration: D, - enforce: true, ...budgets, }); const derived = timetable.getMaxBlocksPerCheckpoint(); diff --git a/yarn-project/stdlib/src/timetable/proposer_timetable.ts b/yarn-project/stdlib/src/timetable/proposer_timetable.ts index f23f71121bff..3379f36e8fd8 100644 --- a/yarn-project/stdlib/src/timetable/proposer_timetable.ts +++ b/yarn-project/stdlib/src/timetable/proposer_timetable.ts @@ -5,7 +5,6 @@ import { ConsensusTimetable, type SlotTimingConstants } from './consensus_timeta /** Result of selecting the next block sub-slot to build. */ export type SubslotSelection = - | { canStart: true; index: number; deadline: undefined; isLastBlock: true } | { canStart: false; index: undefined; deadline: undefined; isLastBlock: false } | { canStart: true; index: number; deadline: number; isLastBlock: boolean }; @@ -33,21 +32,17 @@ export class ProposerTimetable extends ConsensusTimetable { /** Proposer initialization budget (`checkpoint_proposal_init_time`) reserved before the first sub-slot, in seconds. */ public readonly checkpointProposalInitTime: number; - /** Whether the proposer enforces sub-slot/start deadlines (false keeps the single-mined-block test mode). */ - public readonly enforce: boolean; - /** Maximum number of full-duration block sub-slots derivable from this timing config. */ public readonly maxBlocksPerCheckpoint: number; constructor(opts: { l1Constants: SlotTimingConstants; - blockDuration: number | undefined; + blockDuration: number; minBlockDuration: number; p2pPropagationTime: number; checkpointProposalPrepareTime: number; checkpointProposalInitTime: number; checkpointProposalSyncGrace?: number; - enforce: boolean; }) { super({ l1Constants: opts.l1Constants, @@ -67,29 +62,26 @@ export class ProposerTimetable extends ConsensusTimetable { this.p2pPropagationTime = budgets.p2pPropagationTime; this.checkpointProposalPrepareTime = budgets.checkpointProposalPrepareTime; this.checkpointProposalInitTime = budgets.checkpointProposalInitTime; - this.enforce = opts.enforce; // Clamp min block duration to the block duration so a single sub-slot is always startable. - this.minBlockDuration = - this.blockDuration !== undefined - ? Math.min(budgets.minBlockDuration, this.blockDuration) - : budgets.minBlockDuration; + this.minBlockDuration = Math.min(budgets.minBlockDuration, this.blockDuration); this.maxBlocksPerCheckpoint = this.computeMaxBlocksPerCheckpoint(); + if (this.maxBlocksPerCheckpoint < 1) { + throw new Error( + `Invalid timing configuration: derived ${this.maxBlocksPerCheckpoint} blocks per checkpoint for ` + + `slot duration ${this.aztecSlotDuration}s and block duration ${this.blockDuration}s.`, + ); + } } /** * Computes the maximum number of full-duration block sub-slots in a checkpoint from the already-resolved * budgets. Derived from the spec's `max_blocks_per_checkpoint = floor((last_block_build_time - * first_subslot_start) / D)`, where the first sub-slot starts one `checkpoint_proposal_init_time` (`init`) - * after `build_frame_start`, so it simplifies to `floor((S - init - D - 2P - prepCp) / D)`. Single-block - * mode (`blockDuration` undefined) returns 1. + * after `build_frame_start`, so it simplifies to `floor((S - init - D - 2P - prepCp) / D)`. */ private computeMaxBlocksPerCheckpoint(): number { - if (this.blockDuration === undefined) { - return 1; - } - // last_block_build_time - (build_frame_start + init) = S - init - D - 2P - prepCp. const timeAvailableForBlocks = this.aztecSlotDuration - @@ -103,14 +95,13 @@ export class ProposerTimetable extends ConsensusTimetable { /** * Ideal time the last block must finish building by to make the ideal L1 publish path: * `target_slot_start - E - D - 2P - prepCp` (= `checkpoint_proposal_send_time - prepCp`). Single value; - * the proposer sizes block production around the ideal L1-publish path only. In single-block mode - * (`blockDuration` undefined) the `D` term drops out, since the single block is itself the final block. + * the proposer sizes block production around the ideal L1-publish path only. */ public getLastBlockBuildTime(slot: SlotNumber): number { return ( this.getTargetSlotStart(slot) - this.ethereumSlotDuration - - (this.blockDuration ?? 0) - + this.blockDuration - 2 * this.p2pPropagationTime - this.checkpointProposalPrepareTime ); @@ -123,15 +114,9 @@ export class ProposerTimetable extends ConsensusTimetable { * intentionally earlier (by `P`) than the consensus receive gate would strictly allow, and * conservatively no later than the final sub-slot's start cutoff in {@link selectNextSubslot}. * - * Single-block mode (`blockDuration` undefined): `attestation_deadline - 2 * min_block_duration`, - * matching {@link selectNextSubslot}'s single-block branch (which needs `min_block_duration` for - * execution and another for re-execution before the attestation deadline). This keeps the build-entry - * start gate from abandoning a slot that {@link selectNextSubslot} would still allow to start. */ public getBuildStartDeadline(slot: SlotNumber): number { - return this.blockDuration === undefined - ? this.getAttestationDeadline(slot) - 2 * this.minBlockDuration - : this.getLastBlockBuildTime(slot) - this.minBlockDuration; + return this.getLastBlockBuildTime(slot) - this.minBlockDuration; } /** Ideal L1 publish/send time: `target_slot_start - E`. Also the ideal attestation-receipt target. */ @@ -147,11 +132,7 @@ export class ProposerTimetable extends ConsensusTimetable { * prologue finishes rather than being eaten by it. */ public getBlockBuildDeadline(slot: SlotNumber, blockIndex: number): number { - return ( - this.getBuildFrameStart(slot) + - this.checkpointProposalInitTime + - (blockIndex + 1) * this.requireBlockDurationForSchedule() - ); + return this.getBuildFrameStart(slot) + this.checkpointProposalInitTime + (blockIndex + 1) * this.blockDuration; } /** Latest time to keep waiting for txs for sub-slot `k`: `block_build_deadline(k) - min_block_duration`. */ @@ -170,29 +151,10 @@ export class ProposerTimetable extends ConsensusTimetable { * Scans sub-slots in order and picks the first whose build deadline is at least `min_block_duration` * in the future. Sub-slots with insufficient remaining headroom are skipped. * - * When enforcement is disabled, always allows building a single block with no deadline (test/sandbox - * mode). In single-block mode (`blockDuration === undefined`) enforced, splits the remaining time - * between execution and re-execution against the attestation deadline. - * * @param slot - Target slot the checkpoint commits to. * @param now - Current wall-clock time in seconds. */ public selectNextSubslot(slot: SlotNumber, now: number): SubslotSelection { - if (!this.enforce) { - return { canStart: true, index: 0, deadline: undefined, isLastBlock: true }; - } - - if (this.blockDuration === undefined) { - // Single-block enforced mode: execution and re-execution run sequentially, so split the time - // remaining until the attestation deadline in half. - const maxAllowed = this.getAttestationDeadline(slot); - const available = (maxAllowed - now) / 2; - const canStart = available >= this.minBlockDuration; - return canStart - ? { canStart: true, index: 0, deadline: now + available, isLastBlock: true } - : { canStart: false, index: undefined, deadline: undefined, isLastBlock: false }; - } - const maxBlocks = this.maxBlocksPerCheckpoint; for (let index = 0; index < maxBlocks; index++) { const deadline = this.getBlockBuildDeadline(slot, index); @@ -204,11 +166,4 @@ export class ProposerTimetable extends ConsensusTimetable { return { canStart: false, index: undefined, deadline: undefined, isLastBlock: false }; } - - private requireBlockDurationForSchedule(): number { - if (this.blockDuration === undefined) { - throw new Error('blockDuration is required for sub-slot scheduling'); - } - return this.blockDuration; - } } diff --git a/yarn-project/stdlib/src/tx/fee_provider.ts b/yarn-project/stdlib/src/tx/fee_provider.ts index 4cd4d8f29b46..852ee2923c37 100644 --- a/yarn-project/stdlib/src/tx/fee_provider.ts +++ b/yarn-project/stdlib/src/tx/fee_provider.ts @@ -5,6 +5,6 @@ import type { GasFees } from '../gas/gas_fees.js'; export interface FeeProvider { /** Returns the current minimum fees for inclusion in the next block. */ getCurrentMinFees(): Promise; - /** Returns predicted min fees for each slot in the prediction window. */ + /** Returns current min fees first, followed by predicted min fees for each slot in the prediction window. */ getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise; } diff --git a/yarn-project/stdlib/src/tx/validator/error_texts.ts b/yarn-project/stdlib/src/tx/validator/error_texts.ts index bcc100c2d9b0..3c247999358a 100644 --- a/yarn-project/stdlib/src/tx/validator/error_texts.ts +++ b/yarn-project/stdlib/src/tx/validator/error_texts.ts @@ -2,7 +2,8 @@ export const TX_ERROR_INSUFFICIENT_FEE_PER_GAS = 'Insufficient fee per gas'; export const TX_ERROR_INSUFFICIENT_FEE_PAYER_BALANCE = 'Insufficient fee payer balance'; export const TX_ERROR_INSUFFICIENT_GAS_LIMIT = 'Gas limit is below the minimum fixed cost'; -export const TX_ERROR_GAS_LIMIT_TOO_HIGH = 'Gas limit is higher than the amount of gas that the AVM can process'; +export const TX_ERROR_GAS_LIMIT_TOO_HIGH = + 'Gas limit is higher than the maximum amount of gas this network allows per tx'; // Phases export const TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED = 'Setup function not on allow list'; diff --git a/yarn-project/txe/src/index.ts b/yarn-project/txe/src/index.ts index 73c0b35afa7d..1ef4abeece02 100644 --- a/yarn-project/txe/src/index.ts +++ b/yarn-project/txe/src/index.ts @@ -35,7 +35,7 @@ export type TXEForeignCallInput = { inputs: ForeignCallArgs; }; -export const TXEForeignCallInputSchema = zodFor()( +export const TXEForeignCallInputSchema: z.ZodType = zodFor()( z.object({ // Nargo generates session_id as a u64, which may exceed Number.MAX_SAFE_INTEGER. // Zod 4's `.int()` enforces the safe-integer bound, so we drop it here and only require diff --git a/yarn-project/txe/src/oracle/test-resolver/resolver.ts b/yarn-project/txe/src/oracle/test-resolver/resolver.ts index 9b0ca4b28771..70d6b3a21c2f 100644 --- a/yarn-project/txe/src/oracle/test-resolver/resolver.ts +++ b/yarn-project/txe/src/oracle/test-resolver/resolver.ts @@ -33,7 +33,7 @@ export class OracleTestResolver { constructor( private readonly registry: Record, - private readonly fixtures: Record, + private readonly fixtures: Partial>, logger?: Logger, ) { this.logger = logger ?? createLogger('txe:test-resolver'); diff --git a/yarn-project/txe/src/oracle/txe_oracle_registry.ts b/yarn-project/txe/src/oracle/txe_oracle_registry.ts index ad57134bb89f..7b7bd3fd9dd1 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_registry.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_registry.ts @@ -146,7 +146,7 @@ const TXE_PRIVATE_EVENTS: TypeMapping = { }, }; -export const TXE_ORACLE_REGISTRY = { +export const TXE_ORACLE_REGISTRY: Record = { ...ORACLE_REGISTRY, aztec_txe_assertCompatibleOracleVersion: makeEntry({ @@ -368,7 +368,7 @@ export const TXE_ORACLE_REGISTRY = { params: [{ name: 'address', type: AZTEC_ADDRESS }], returnType: CONTRACT_INSTANCE_MEMBER, }), -} satisfies Record; +}; /** * Deserializes oracle inputs, calls the handler with typed params, serializes the result, and wraps diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 0ffebc718581..8ecdf2eaffc9 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -174,7 +174,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl return (await this.stateMachine.node.getBlockData('latest'))!.header.globalVariables.timestamp; } - async getLastTxEffects() { + async getLastTxEffects(): ReturnType { const latestBlockNumber = await this.stateMachine.archiver.getBlockNumber(); const block = await this.stateMachine.archiver.getBlock({ number: latestBlockNumber }); diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index 4e33242b6de0..7d3bd75e23c0 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -83,10 +83,7 @@ export class TXEStateMachine { public get l2TipsProvider(): L2TipsProvider { const node = this.node; return { - getL2Tips: async () => { - const tips = await node.getChainTips(); - return { ...tips, proposedCheckpoint: tips.checkpointed }; - }, + getL2Tips: () => node.getChainTips(), }; } diff --git a/yarn-project/validator-client/README.md b/yarn-project/validator-client/README.md index 4205a31e117e..06afb9a0328d 100644 --- a/yarn-project/validator-client/README.md +++ b/yarn-project/validator-client/README.md @@ -226,6 +226,8 @@ This is useful for monitoring network health without participating in consensus. L1 enforces gas and blob capacity per checkpoint. The node enforces these during block building to avoid L1 rejection. Three dimensions are metered: L2 gas (mana), DA gas, and blob fields. DA gas maps to blob fields today (`daGas = blobFields * 32`) but both are tracked independently. +The full per-tx → per-block → per-checkpoint limits hierarchy, including how the per-block budgets relate to the network admission limits, is documented in [`stdlib/src/gas/README.md`](../stdlib/src/gas/README.md) under "Gas and Data Limits". + ### Checkpoint limits | Dimension | Source | Budget | diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index e5151c230e6e..6f5d48bdacff 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -1,9 +1,11 @@ import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding'; import { BLOBS_PER_CHECKPOINT, + CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, DA_GAS_PER_FIELD, FIELDS_PER_BLOB, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, + TX_DA_GAS_OVERHEAD, } from '@aztec/constants'; import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -118,6 +120,7 @@ describe('CheckpointBuilder', () => { minValidTxs?: number; maxBlocksPerCheckpoint?: number; perBlockAllocationMultiplier?: number; + perBlockDAAllocationMultiplier?: number; }, ): BlockBuilderOptions { return { @@ -125,6 +128,7 @@ describe('CheckpointBuilder', () => { isBuildingProposal: true, maxBlocksPerCheckpoint: overrides?.maxBlocksPerCheckpoint ?? 5, perBlockAllocationMultiplier: overrides?.perBlockAllocationMultiplier ?? 1.2, + perBlockDAAllocationMultiplier: overrides?.perBlockDAAllocationMultiplier, minValidTxs: overrides?.minValidTxs ?? 0, }; } @@ -744,4 +748,42 @@ describe('CheckpointBuilder', () => { expect(capped.maxTransactions).toBe(30); }); }); + + describe('per-block DA allocation multiplier (largest deploy fit under v5 mainnet geometry)', () => { + // v5 mainnet: 72s slots / 6s blocks -> 10 blocks per checkpoint. + const mainnetBlocks = 10; + // Largest tx we want to support: a maximal contract class registration, dominated by its contract class + // log (content + contract-address field) plus the fixed tx overhead. Deploy-side nullifiers add a few + // more fields, so this is a lower bound on the true largest deploy. + const largestDeployBlobFields = CONTRACT_CLASS_LOG_SIZE_IN_FIELDS + 1 + TX_DA_GAS_OVERHEAD / DA_GAS_PER_FIELD; + const largestDeployDaGas = largestDeployBlobFields * DA_GAS_PER_FIELD; + + it('fits the largest contract class deploy in DA gas and blob fields with the 1.5 DA multiplier', () => { + setupBuilder(); + lightweightCheckpointBuilder.getBlocks.mockReturnValue([]); + + const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits( + proposerOpts({ + maxBlocksPerCheckpoint: mainnetBlocks, + perBlockAllocationMultiplier: 1.2, + perBlockDAAllocationMultiplier: 1.5, + }), + ); + + expect(capped.maxBlockGas!.daGas).toBeGreaterThanOrEqual(largestDeployDaGas); + expect(capped.maxBlobFields).toBeGreaterThanOrEqual(largestDeployBlobFields); + }); + + it('does not fit the largest contract class deploy with only the general 1.2 multiplier', () => { + setupBuilder(); + lightweightCheckpointBuilder.getBlocks.mockReturnValue([]); + + const capped = (checkpointBuilder as TestCheckpointBuilder).testCapLimits( + proposerOpts({ maxBlocksPerCheckpoint: mainnetBlocks, perBlockAllocationMultiplier: 1.2 }), + ); + + expect(capped.maxBlockGas!.daGas).toBeLessThan(largestDeployDaGas); + expect(capped.maxBlobFields!).toBeLessThan(largestDeployBlobFields); + }); + }); }); diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 05489c21e809..d2bcd480aec2 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -219,10 +219,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { if (opts.isBuildingProposal) { const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length); const multiplier = opts.perBlockAllocationMultiplier; + // DA gas and blob fields use a higher multiplier so the largest contract class deploy fits a block. + const daMultiplier = opts.perBlockDAAllocationMultiplier ?? multiplier; cappedL2Gas = Math.min(cappedL2Gas, Math.ceil((remainingMana / remainingBlocks) * multiplier)); - cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * multiplier)); - cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * multiplier)); + cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * daMultiplier)); + cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * daMultiplier)); cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier)); } diff --git a/yarn-project/validator-client/src/factory.ts b/yarn-project/validator-client/src/factory.ts index 404574992322..f1858ef8e8f1 100644 --- a/yarn-project/validator-client/src/factory.ts +++ b/yarn-project/validator-client/src/factory.ts @@ -35,7 +35,7 @@ export function createProposalHandler( const metrics = new ValidatorMetrics(deps.telemetry); const consensusTimetable = new ConsensusTimetable({ l1Constants: deps.epochCache.getL1Constants(), - blockDuration: config.blockDurationMs !== undefined ? config.blockDurationMs / 1000 : undefined, + blockDuration: config.blockDurationMs / 1000, }); const blockProposalValidator = new BlockProposalValidator(deps.epochCache, consensusTimetable, { txsPermitted: !config.disableTransactions, diff --git a/yarn-project/validator-client/src/proposal_handler.test.ts b/yarn-project/validator-client/src/proposal_handler.test.ts index 85a22e54cac3..f45281b26aec 100644 --- a/yarn-project/validator-client/src/proposal_handler.test.ts +++ b/yarn-project/validator-client/src/proposal_handler.test.ts @@ -90,7 +90,7 @@ describe('ProposalHandler checkpoint validation', () => { rollupAddress: TEST_COORDINATION_SIGNATURE_CONTEXT.rollupAddress, } as ValidatorClientFullConfig; - consensusTimetable = new ConsensusTimetable({ l1Constants: epochCache.getL1Constants(), blockDuration: undefined }); + consensusTimetable = new ConsensusTimetable({ l1Constants: epochCache.getL1Constants(), blockDuration: 3 }); handler = new ProposalHandler( checkpointsBuilder, diff --git a/yarn-project/validator-client/src/validator.ha.integration.test.ts b/yarn-project/validator-client/src/validator.ha.integration.test.ts index ec2d683c634a..983b08972f7a 100644 --- a/yarn-project/validator-client/src/validator.ha.integration.test.ts +++ b/yarn-project/validator-client/src/validator.ha.integration.test.ts @@ -144,9 +144,10 @@ describe('ValidatorClient HA Integration', () => { | 'slashDuplicateProposalPenalty' | 'slashDuplicateAttestationPenalty' | 'slashAttestInvalidCheckpointProposalPenalty' - > = { + > & { blockDurationMs: number } = { validatorPrivateKeys: new SecretValue(validatorPrivateKeys), attestationPollingIntervalMs: 1000, + blockDurationMs: 3000, disableValidator: false, disabledValidators: [], slashBroadcastedInvalidBlockPenalty: 1n, @@ -203,7 +204,7 @@ describe('ValidatorClient HA Integration', () => { | 'slashDuplicateProposalPenalty' | 'slashDuplicateAttestationPenalty' | 'slashAttestInvalidCheckpointProposalPenalty' - >, + > & { blockDurationMs: number }, ): Promise { // Track pool for cleanup pools.push(pool); @@ -220,7 +221,7 @@ describe('ValidatorClient HA Integration', () => { const metrics = new ValidatorMetrics(getTelemetryClient()); const consensusTimetable = new ConsensusTimetable({ l1Constants: epochCache.getL1Constants(), - blockDuration: undefined, + blockDuration: 3, }); const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, { txsPermitted: true, diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index e6359f6554a9..29f11ec711e0 100644 --- a/yarn-project/validator-client/src/validator.integration.test.ts +++ b/yarn-project/validator-client/src/validator.integration.test.ts @@ -185,6 +185,7 @@ describe('ValidatorClient Integration', () => { l1ChainId: chainId.toNumber(), validatorPrivateKeys: new SecretValue([privateKey]), attestationPollingIntervalMs: 100, + blockDurationMs: 3000, disableValidator: false, disabledValidators: [], slashBroadcastedInvalidBlockPenalty: 10n, diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index 6e4e42a1703e..354685f501b0 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -95,6 +95,7 @@ describe('ValidatorClient', () => { | 'slashAttestInvalidCheckpointProposalPenalty' > & { disableTransactions: boolean; + blockDurationMs: number; }; let validatorClient: ValidatorClient; let p2pClient: MockProxy; @@ -185,6 +186,7 @@ describe('ValidatorClient', () => { config = { validatorPrivateKeys: new SecretValue(validatorPrivateKeys), attestationPollingIntervalMs: 1000, + blockDurationMs: 3000, disableValidator: false, disabledValidators: [], slashBroadcastedInvalidBlockPenalty: 1n, diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index d12bf312fc5e..732c2cd28beb 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -250,7 +250,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) const metrics = new ValidatorMetrics(telemetry); const consensusTimetable = new ConsensusTimetable({ l1Constants: epochCache.getL1Constants(), - blockDuration: config.blockDurationMs !== undefined ? config.blockDurationMs / 1000 : undefined, + blockDuration: config.blockDurationMs / 1000, }); const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, { txsPermitted: !config.disableTransactions, diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts index 6c4b7ec561b9..9451aa85b738 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts @@ -1,4 +1,4 @@ -import type { Account } from '@aztec/aztec.js/account'; +import { type Account, NO_FROM } from '@aztec/aztec.js/account'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { Aliased } from '@aztec/aztec.js/wallet'; import { BlockNumber } from '@aztec/foundation/branded-types'; @@ -29,7 +29,7 @@ import { import { type MockProxy, mock } from 'jest-mock-extended'; -import { BaseWallet } from './base_wallet.js'; +import { BaseWallet, type CompleteFeeOptionsConfig, type FeeOptions } from './base_wallet.js'; class BasicWallet extends BaseWallet { mockAccount = mock(); @@ -49,6 +49,14 @@ class BasicWallet extends BaseWallet { public override getMinFees(estimate?: ManaUsageEstimate): Promise { return super.getMinFees(estimate); } + + public getMaxTxGasLimitsForTest(): Promise { + return super.getMaxTxGasLimits(); + } + + public completeFeeOptionsForTest(config: CompleteFeeOptionsConfig): Promise { + return super.completeFeeOptions(config); + } } async function makeFunctionCall(type: FunctionType, isStatic: boolean, name: string): Promise { @@ -86,7 +94,12 @@ describe('BaseWallet', () => { node.getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]); node.getCurrentMinFees.mockResolvedValue(new GasFees(2, 2)); - node.getNodeInfo.mockResolvedValue({ ...mock(), l1ChainId: 1, rollupVersion: 1 }); + node.getNodeInfo.mockResolvedValue({ + ...mock(), + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, + }); pxe.getSyncedBlockHeader.mockResolvedValue(BlockHeader.empty()); wallet.mockAccount.createTxExecutionRequest.mockResolvedValue(mock()); @@ -244,6 +257,105 @@ describe('BaseWallet', () => { }); }); + describe('node info caching', () => { + it('refetches node info after a rejected fetch instead of caching the rejection', async () => { + pxe = mock(); + node = mock(); + const wallet = new BasicWallet(pxe, node); + + node.getNodeInfo.mockRejectedValueOnce(new Error('node unavailable')).mockResolvedValue({ + ...mock(), + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, + }); + + await expect(wallet.getMaxTxGasLimitsForTest()).rejects.toThrow('node unavailable'); + + const gas = await wallet.getMaxTxGasLimitsForTest(); + expect(gas).toEqual(new Gas(117_668, 6_540_000)); + expect(node.getNodeInfo).toHaveBeenCalledTimes(2); + }); + + it('caches a successful node info fetch for subsequent calls', async () => { + pxe = mock(); + node = mock(); + const wallet = new BasicWallet(pxe, node); + + node.getNodeInfo.mockResolvedValue({ + ...mock(), + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, + }); + + await wallet.getMaxTxGasLimitsForTest(); + await wallet.getChainInfo(); + expect(node.getNodeInfo).toHaveBeenCalledTimes(1); + }); + }); + + describe('completeFeeOptions gas limit validation', () => { + let pxe: MockProxy; + let node: MockProxy; + let wallet: BasicWallet; + + beforeEach(() => { + pxe = mock(); + node = mock(); + wallet = new BasicWallet(pxe, node); + node.getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]); + node.getCurrentMinFees.mockResolvedValue(new GasFees(2, 2)); + node.getNodeInfo.mockResolvedValue({ + ...mock(), + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 1000, l2Gas: 2000 } }, + }); + }); + + it('fills in the network admission limit when no gas limits are declared', async () => { + const { gasSettings } = await wallet.completeFeeOptionsForTest({ from: NO_FROM }); + expect(gasSettings.gasLimits).toEqual(new Gas(1000, 2000)); + }); + + it('accepts caller-provided gas limits at or below the network admission limit', async () => { + const { gasSettings } = await wallet.completeFeeOptionsForTest({ + from: NO_FROM, + gasSettings: { gasLimits: Gas.from({ daGas: 1000, l2Gas: 2000 }) }, + }); + expect(gasSettings.gasLimits).toEqual(new Gas(1000, 2000)); + }); + + it('rejects caller-provided da gas limit above the network admission limit', async () => { + await expect( + wallet.completeFeeOptionsForTest({ + from: NO_FROM, + gasSettings: { gasLimits: Gas.from({ daGas: 1001, l2Gas: 2000 }) }, + }), + ).rejects.toThrow('Declared DA gas limit (1001) exceeds the maximum this network allows per tx (1000)'); + }); + + it('rejects caller-provided l2 gas limit above the network admission limit', async () => { + await expect( + wallet.completeFeeOptionsForTest({ + from: NO_FROM, + gasSettings: { gasLimits: Gas.from({ daGas: 1000, l2Gas: 2001 }) }, + }), + ).rejects.toThrow('Declared L2 gas limit (2001) exceeds the maximum this network allows per tx (2000)'); + }); + + it('does not validate against the admission limit when estimating', async () => { + await expect( + wallet.completeFeeOptionsForTest({ + from: NO_FROM, + forEstimation: true, + gasSettings: { gasLimits: Gas.from({ daGas: 1_000_000, l2Gas: 1_000_000 }) }, + }), + ).resolves.toBeDefined(); + }); + }); + it('should extract offchain messages with anchor block timestamp on sendTx', async () => { pxe = mock(); node = mock(); @@ -278,7 +390,12 @@ describe('BaseWallet', () => { // Mock dependencies for completeFeeOptions and createTxExecutionRequestFromPayloadAndFee node.getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]); node.getCurrentMinFees.mockResolvedValue(new GasFees(2, 2)); - node.getNodeInfo.mockResolvedValue({ ...mock(), l1ChainId: 1, rollupVersion: 1 }); + node.getNodeInfo.mockResolvedValue({ + ...mock(), + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, + }); pxe.getSyncedBlockHeader.mockResolvedValue(BlockHeader.empty()); wallet.mockAccount.createTxExecutionRequest.mockResolvedValue(mock()); pxe.proveTx.mockResolvedValue(provenTx); diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 94ca6f079210..29f220531915 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -65,6 +65,7 @@ import { import { inspect } from 'util'; +import { assertGasLimitsWithinNetworkLimits } from './get_gas_limits.js'; import { buildMergedSimulationResult, extractOptimizablePublicStaticCalls, simulateViaNode } from './utils.js'; /** @@ -158,14 +159,38 @@ export abstract class BaseWallet implements Wallet { return senders.map(sender => ({ item: sender, alias: '' })); } - async getChainInfo(): Promise { + /** + * Fetches and caches the node info for the wallet's lifetime, since a wallet talks to a single network and + * node info never changes. A rejected fetch clears the cache so the next call retries instead of replaying + * the cached rejection forever — important because the gas-limit fill-in and validation (run on every send) + * depend on it. + */ + private getNodeInfo(): Promise { if (!this.nodeInfoPromise) { - this.nodeInfoPromise = this.aztecNode.getNodeInfo(); + this.nodeInfoPromise = this.aztecNode.getNodeInfo().catch(err => { + this.nodeInfoPromise = undefined; + throw err; + }); } - const { l1ChainId, rollupVersion } = await this.nodeInfoPromise; + return this.nodeInfoPromise; + } + + async getChainInfo(): Promise { + const { l1ChainId, rollupVersion } = await this.getNodeInfo(); return { chainId: new Fr(l1ChainId), version: new Fr(rollupVersion) }; } + /** + * Returns the maximum gas limits a single transaction may declare on this wallet's network (the + * node-advertised `txsLimits.gas`). Internal helper used to fill in default gas limits when sending a + * transaction without explicit limits, and to validate caller-provided limits before sending. Backed by + * the cached node info, since a wallet talks to a single network. + */ + protected async getMaxTxGasLimits(): Promise { + const { txsLimits } = await this.getNodeInfo(); + return new Gas(txsLimits.gas.daGas, txsLimits.gas.l2Gas); + } + protected async createTxExecutionRequestFromPayloadAndFee( executionPayload: ExecutionPayload, from: AztecAddress | NoFrom, @@ -272,10 +297,25 @@ export abstract class BaseWallet implements Wallet { maxPriorityFeesPerGas: gasSettings?.maxPriorityFeesPerGas ?? GasFees.empty(), }; // When estimating gas (simulation), use high limits so the simulation doesn't run out of gas. - // When sending for real, use protocol max limits that the network will actually accept. - const fullGasSettings = forEstimation - ? GasSettings.forEstimation(gasSettingsOverrides) - : GasSettings.fallback(gasSettingsOverrides); + // When sending for real without explicit limits, declare the most a single tx may use on this network + // (the node's per-tx admission limit), so the proposer does not skip the tx for over-declaring gas. + let fullGasSettings; + if (forEstimation) { + // Estimation deliberately uses very high internal limits and skips tx validation, so we do not + // validate against the network admission limit here. + fullGasSettings = GasSettings.forEstimation(gasSettingsOverrides); + } else { + const maxTxGasLimits = await this.getMaxTxGasLimits(); + // If the caller declared explicit gas limits, reject them up front when they exceed the network's + // per-tx admission limit (mirroring the node's GasLimitsValidator). Otherwise fill in the limit. + if (gasSettingsOverrides.gasLimits) { + assertGasLimitsWithinNetworkLimits(gasSettingsOverrides.gasLimits, maxTxGasLimits); + } + fullGasSettings = GasSettings.fallback({ + ...gasSettingsOverrides, + gasLimits: gasSettingsOverrides.gasLimits ?? maxTxGasLimits, + }); + } this.log.debug(`Using L2 gas settings`, fullGasSettings); return { gasSettings: fullGasSettings, diff --git a/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.test.ts b/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.test.ts new file mode 100644 index 000000000000..e34e0a7d80b3 --- /dev/null +++ b/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.test.ts @@ -0,0 +1,156 @@ +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; +import { Gas, type GasUsed } from '@aztec/stdlib/gas'; + +import { assertGasLimitsWithinNetworkLimits, getGasLimits } from './get_gas_limits.js'; + +describe('getGasLimits', () => { + let gasUsed: GasUsed; + + // A network limit comfortably above the mocked usage, so padding is never clamped. + const maxTxGasLimits = Gas.from({ daGas: 117_668, l2Gas: 6_540_000 }); + + beforeEach(() => { + gasUsed = { + totalGas: Gas.from({ daGas: 140, l2Gas: 280 }), + // Assume teardown gas limit of 20, 30 + billedGas: Gas.from({ daGas: 150, l2Gas: 290 }), + teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + }); + + it('returns gas limits from private gas usage only', () => { + gasUsed = { + totalGas: Gas.from({ daGas: 100, l2Gas: 200 }), + billedGas: Gas.from({ daGas: 100, l2Gas: 200 }), + teardownGas: Gas.empty(), + publicGas: Gas.empty(), + }; + // Should be 110 and 220 but oh floating point + expect(getGasLimits(gasUsed, maxTxGasLimits)).toEqual({ + gasLimits: Gas.from({ daGas: 111, l2Gas: 221 }), + teardownGasLimits: Gas.empty(), + }); + }); + + it('returns gas limits for private and public', () => { + expect(getGasLimits(gasUsed, maxTxGasLimits)).toEqual({ + gasLimits: Gas.from({ daGas: 154, l2Gas: 308 }), + teardownGasLimits: Gas.from({ daGas: 11, l2Gas: 22 }), + }); + }); + + it('pads gas limits in full when below the network limit', () => { + expect(getGasLimits(gasUsed, maxTxGasLimits, 1)).toEqual({ + gasLimits: Gas.from({ daGas: 280, l2Gas: 560 }), + teardownGasLimits: Gas.from({ daGas: 20, l2Gas: 40 }), + }); + }); + + it('clamps padded gas at the network limit when usage is below it', () => { + // Usage fits the network limit, but padding it would exceed it; the declared limit is clamped down. + const tightLimits = Gas.from({ daGas: 145, l2Gas: 290 }); + expect(getGasLimits(gasUsed, tightLimits, 0.1)).toEqual({ + // 140 * 1.1 = 154 -> clamped to 145; 280 * 1.1 = 308 -> clamped to 290. + gasLimits: Gas.from({ daGas: 145, l2Gas: 290 }), + // Teardown is below the limit, so it pads normally. + teardownGasLimits: Gas.from({ daGas: 11, l2Gas: 22 }), + }); + }); + + it('clamps teardown limits at the network limit', () => { + // Total usage fits the limit, but both total and teardown padded values exceed it and get clamped. + gasUsed = { + totalGas: Gas.from({ daGas: 100, l2Gas: 200 }), + billedGas: Gas.from({ daGas: 110, l2Gas: 210 }), + teardownGas: Gas.from({ daGas: 100, l2Gas: 200 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + const tightLimits = Gas.from({ daGas: 105, l2Gas: 210 }); + expect(getGasLimits(gasUsed, tightLimits, 0.1)).toEqual({ + // 100 * 1.1 = 110 clamped to 105; 200 * 1.1 = 220 clamped to 210. + gasLimits: Gas.from({ daGas: 105, l2Gas: 210 }), + // 100 * 1.1 = 110 clamped to 105; 200 * 1.1 = 220 clamped to 210. + teardownGasLimits: Gas.from({ daGas: 105, l2Gas: 210 }), + }); + }); + + it('throws if simulated da gas exceeds the network admission limit', () => { + gasUsed = { + totalGas: Gas.from({ daGas: 150, l2Gas: 280 }), + billedGas: Gas.from({ daGas: 160, l2Gas: 290 }), + teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + const tightLimits = Gas.from({ daGas: 140, l2Gas: 6_540_000 }); + expect(() => getGasLimits(gasUsed, tightLimits, 0)).toThrow( + 'Transaction consumes 150 DA gas but the network only admits transactions declaring up to 140 DA gas', + ); + }); + + it('clamps caller-supplied limits above the protocol maxima down to the protocol maxima', () => { + // A caller may pass an unclamped maxTxGasLimits above the per-tx protocol maxima; the function must + // defensively clamp to them so the declared limits never exceed what the protocol allows. + const aboveProtocolMaxima = Gas.from({ daGas: MAX_TX_DA_GAS * 2, l2Gas: MAX_PROCESSABLE_L2_GAS * 2 }); + + // Usage above the protocol maximum is still rejected even though it is below the caller-supplied limit. + gasUsed = { + totalGas: Gas.from({ daGas: MAX_TX_DA_GAS + 1, l2Gas: 280 }), + billedGas: Gas.from({ daGas: MAX_TX_DA_GAS + 11, l2Gas: 290 }), + teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + expect(() => getGasLimits(gasUsed, aboveProtocolMaxima, 0)).toThrow( + `Transaction consumes ${MAX_TX_DA_GAS + 1} DA gas but the network only admits transactions declaring up to ${MAX_TX_DA_GAS} DA gas`, + ); + + // Usage at the protocol maximum pads up against the protocol maximum, clamping the padded limit to it. + gasUsed = { + totalGas: Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS }), + billedGas: Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS }), + teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + const { gasLimits } = getGasLimits(gasUsed, aboveProtocolMaxima, 1); + expect(gasLimits).toEqual(Gas.from({ daGas: MAX_TX_DA_GAS, l2Gas: MAX_PROCESSABLE_L2_GAS })); + }); + + it('throws if simulated l2 gas exceeds the network admission limit', () => { + gasUsed = { + totalGas: Gas.from({ daGas: 140, l2Gas: 300 }), + billedGas: Gas.from({ daGas: 150, l2Gas: 310 }), + teardownGas: Gas.from({ daGas: 10, l2Gas: 20 }), + publicGas: Gas.from({ daGas: 50, l2Gas: 200 }), + }; + const tightLimits = Gas.from({ daGas: 117_668, l2Gas: 290 }); + expect(() => getGasLimits(gasUsed, tightLimits, 0)).toThrow( + 'Transaction consumes 300 L2 gas but the network only admits transactions declaring up to 290 L2 gas', + ); + }); +}); + +describe('assertGasLimitsWithinNetworkLimits', () => { + const maxTxGasLimits = Gas.from({ daGas: 1000, l2Gas: 2000 }); + + it('passes when declared limits are below the network limits', () => { + expect(() => + assertGasLimitsWithinNetworkLimits(Gas.from({ daGas: 500, l2Gas: 1000 }), maxTxGasLimits), + ).not.toThrow(); + }); + + it('passes when declared limits equal the network limits', () => { + expect(() => assertGasLimitsWithinNetworkLimits(maxTxGasLimits, maxTxGasLimits)).not.toThrow(); + }); + + it('throws when declared da gas exceeds the network limit', () => { + expect(() => assertGasLimitsWithinNetworkLimits(Gas.from({ daGas: 1001, l2Gas: 2000 }), maxTxGasLimits)).toThrow( + 'Declared DA gas limit (1001) exceeds the maximum this network allows per tx (1000)', + ); + }); + + it('throws when declared l2 gas exceeds the network limit', () => { + expect(() => assertGasLimitsWithinNetworkLimits(Gas.from({ daGas: 1000, l2Gas: 2001 }), maxTxGasLimits)).toThrow( + 'Declared L2 gas limit (2001) exceeds the maximum this network allows per tx (2000)', + ); + }); +}); diff --git a/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.ts b/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.ts new file mode 100644 index 000000000000..4233325270a7 --- /dev/null +++ b/yarn-project/wallet-sdk/src/base-wallet/get_gas_limits.ts @@ -0,0 +1,88 @@ +import { MAX_PROCESSABLE_L2_GAS, MAX_TX_DA_GAS } from '@aztec/constants'; +import { Gas, type GasUsed } from '@aztec/stdlib/gas'; + +/** + * Returns suggested total and teardown gas limits for a simulated tx, clamped to the network's per-tx + * admission limits. + * + * The network only admits transactions that declare up to `maxTxGasLimits` per dimension (the + * node-advertised `txsLimits.gas`). Wallets pass the value read from their own node info, but since node info + * is remote input it is defensively clamped here to the per-tx protocol maxima so a value above them is never + * honored. If the simulated usage already exceeds the resulting admission limits the tx can never be included, + * so this throws a descriptive error instead of returning a limit the node would reject. Otherwise it pads the + * usage and clamps each dimension to the admission limit. + * @param gasUsed - The gas actually consumed during simulation. + * @param maxTxGasLimits - The maximum gas a single tx may declare on this network (the node-advertised `txsLimits.gas`). + * @param pad - Fraction to pad the suggested gas limits by (as a decimal, e.g. 0.1 for 10%). The effective + * padding shrinks to zero as usage approaches the network limit, since the network will not admit a higher + * declared limit regardless of the buffer. + */ +export function getGasLimits( + gasUsed: GasUsed, + maxTxGasLimits: Gas, + pad = 0.1, +): { + /** + * Gas limit for the tx, excluding teardown gas + */ + gasLimits: Gas; + /** + * Gas limit for the teardown phase + */ + teardownGasLimits: Gas; +} { + const { totalGas, teardownGas } = gasUsed; + + // `maxTxGasLimits` is the node-advertised admission limit. Node info is remote input, so we defensively + // clamp to the per-tx protocol maxima so a value above them can never be honored. + const maxLimits = new Gas( + Math.min(maxTxGasLimits.daGas, MAX_TX_DA_GAS), + Math.min(maxTxGasLimits.l2Gas, MAX_PROCESSABLE_L2_GAS), + ); + + // The simulated usage must fit within the admission limits, otherwise the tx can never be included. + if (totalGas.daGas > maxLimits.daGas) { + throw new Error( + `Transaction consumes ${totalGas.daGas} DA gas but the network only admits transactions declaring up to ${maxLimits.daGas} DA gas`, + ); + } + if (totalGas.l2Gas > maxLimits.l2Gas) { + throw new Error( + `Transaction consumes ${totalGas.l2Gas} L2 gas but the network only admits transactions declaring up to ${maxLimits.l2Gas} L2 gas`, + ); + } + + // Pad the limits by the buffer, then cap each dimension at the admission limit so the buffer cannot push a + // declared limit past what inbound validation accepts. Teardown is part of the total, so clamping it to the + // admission limit is safe. + return { + gasLimits: padGas(totalGas, pad, maxLimits), + teardownGasLimits: padGas(teardownGas, pad, maxLimits), + }; +} + +/** Pads each gas dimension, capping it at the network admission limit. */ +function padGas(gas: Gas, pad: number, cap: Gas): Gas { + const padded = gas.mul(1 + pad); + return new Gas(Math.min(padded.daGas, cap.daGas), Math.min(padded.l2Gas, cap.l2Gas)); +} + +/** + * Validates that caller-declared gas limits do not exceed the network's per-tx admission limits, throwing a + * descriptive error per dimension when they do. The node's inbound validation checks declared + * `gasSettings.gasLimits`, so we mirror that here to surface the rejection locally before the tx is sent. + * @param gasLimits - The gas limits the transaction will declare. + * @param maxTxGasLimits - The maximum gas a single tx may declare on this network (the node-advertised `txsLimits.gas`). + */ +export function assertGasLimitsWithinNetworkLimits(gasLimits: Gas, maxTxGasLimits: Gas): void { + if (gasLimits.daGas > maxTxGasLimits.daGas) { + throw new Error( + `Declared DA gas limit (${gasLimits.daGas}) exceeds the maximum this network allows per tx (${maxTxGasLimits.daGas})`, + ); + } + if (gasLimits.l2Gas > maxTxGasLimits.l2Gas) { + throw new Error( + `Declared L2 gas limit (${gasLimits.l2Gas}) exceeds the maximum this network allows per tx (${maxTxGasLimits.l2Gas})`, + ); + } +} diff --git a/yarn-project/wallet-sdk/src/base-wallet/index.ts b/yarn-project/wallet-sdk/src/base-wallet/index.ts index dedd20e47277..2d3d16427394 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/index.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/index.ts @@ -5,3 +5,4 @@ export { type SimulateViaEntrypointOptions, } from './base_wallet.js'; export { simulateViaNode, buildMergedSimulationResult, extractOptimizablePublicStaticCalls } from './utils.js'; +export { getGasLimits, assertGasLimitsWithinNetworkLimits } from './get_gas_limits.js'; diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.test.ts b/yarn-project/wallets/src/embedded/embedded_wallet.test.ts index 2dd6c0085784..e91818763460 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.test.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.test.ts @@ -44,7 +44,11 @@ describe('EmbeddedWallet', () => { describe('sendTx', () => { it('passes sendMessagesAs as senderForTags to PXE simulation', async () => { getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]); - getNodeInfo.mockResolvedValue({ l1ChainId: 1, rollupVersion: 1 } as any); + getNodeInfo.mockResolvedValue({ + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 117_668, l2Gas: 6_540_000 } }, + } as any); simulateTx.mockResolvedValue(makeMinimalSimResult()); proveTx.mockRejectedValue(new Error('stop-at-prove')); @@ -68,6 +72,36 @@ describe('EmbeddedWallet', () => { expect.objectContaining({ senderForTags: sendMessagesAs }), ); }); + + it('rejects caller-provided gas limits above the network admission limit', async () => { + getPredictedMinFees.mockResolvedValue([new GasFees(2, 2)]); + getNodeInfo.mockResolvedValue({ + l1ChainId: 1, + rollupVersion: 1, + txsLimits: { gas: { daGas: 1000, l2Gas: 2000 } }, + } as any); + simulateTx.mockResolvedValue(makeMinimalSimResult()); + proveTx.mockRejectedValue(new Error('stop-at-prove')); + + const call = FunctionCall.from({ + name: 'test', + to: await AztecAddress.random(), + selector: FunctionSelector.random(), + type: FunctionType.PRIVATE, + hideMsgSender: false, + isStatic: false, + args: [], + returnTypes: [], + }); + const payload = new ExecutionPayload([call], [], []); + + await expect( + wallet.sendTx(payload, { + from: NO_FROM, + fee: { gasSettings: { gasLimits: Gas.from({ daGas: 5000, l2Gas: 2000 }) } }, + }), + ).rejects.toThrow('Declared DA gas limit (5000) exceeds the maximum this network allows per tx (1000)'); + }); }); }); diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index 1e30d72533ea..bea86f6c6e57 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -1,12 +1,6 @@ import { type Account, NO_FROM } from '@aztec/aztec.js/account'; import { CallAuthorizationRequest } from '@aztec/aztec.js/authorization'; -import { - type InteractionWaitOptions, - NO_WAIT, - type SendReturn, - type WaitOpts, - getGasLimits, -} from '@aztec/aztec.js/contracts'; +import { type InteractionWaitOptions, NO_WAIT, type SendReturn, type WaitOpts } from '@aztec/aztec.js/contracts'; import type { Aliased, ExecuteUtilityOptions, @@ -41,7 +35,7 @@ import { collectOffchainEffects, mergeExecutionPayloads, } from '@aztec/stdlib/tx'; -import { BaseWallet, type SimulateViaEntrypointOptions } from '@aztec/wallet-sdk/base-wallet'; +import { BaseWallet, type SimulateViaEntrypointOptions, getGasLimits } from '@aztec/wallet-sdk/base-wallet'; import type { AccountContractsProvider } from './account-contract-providers/types.js'; import { type AccountType, WalletDB } from './wallet_db.js'; @@ -191,7 +185,8 @@ export class EmbeddedWallet extends BaseWallet { executionPayload.authWitnesses.push(authwit); } } - const estimated = getGasLimits(simulationResult, this.estimatedGasPadding); + const maxTxGasLimits = await this.getMaxTxGasLimits(); + const estimated = getGasLimits(simulationResult.gasUsed, maxTxGasLimits, this.estimatedGasPadding); this.log.verbose( `Estimated gas limits for tx: DA=${estimated.gasLimits.daGas} L2=${estimated.gasLimits.l2Gas} teardownDA=${estimated.teardownGasLimits.daGas} teardownL2=${estimated.teardownGasLimits.l2Gas}`, ); diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index 42f5471d609c..07099fc6b150 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -297,16 +297,10 @@ class TestWorldStateSynchronizer extends ServerWorldStateSynchronizer { } public override getL2Tips() { - const makeTipId = (blockId: typeof this.latest) => ({ - block: blockId, - checkpoint: { number: CheckpointNumber.fromBlockNumber(blockId.number), hash: blockId.hash }, - }); return Promise.resolve({ proposed: this.latest, - checkpointed: makeTipId(this.latest), - proven: makeTipId(this.proven), - finalized: makeTipId(this.finalized), - proposedCheckpoint: makeTipId(this.latest), + proven: { block: this.proven }, + finalized: { block: this.finalized }, }); } } diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index f46fe2c1b584..429f511a288d 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -1,4 +1,3 @@ -import { INITIAL_CHECKPOINT_NUMBER } from '@aztec/constants'; import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -6,15 +5,13 @@ import { promiseWithResolvers } from '@aztec/foundation/promise'; import { elapsed } from '@aztec/foundation/timer'; import { type BlockHash, - GENESIS_CHECKPOINT_HEADER_HASH, type L2Block, - type L2BlockId, type L2BlockSource, L2BlockStream, type L2BlockStreamEvent, type L2BlockStreamEventHandler, type L2BlockStreamLocalDataProvider, - type L2Tips, + type LocalChainTips, } from '@aztec/stdlib/block'; import { WorldStateRunningState, @@ -266,8 +263,12 @@ export class ServerWorldStateSynchronizer return this.merkleTreeCommitted.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(number)).then(leaf => leaf?.toString()); } - /** Returns the latest L2 block number for each tip of the chain (latest, proven, finalized). */ - public async getL2Tips(): Promise { + /** + * Returns the proposed, proven, and finalized block tips of the chain. World state drives its block stream with + * `ignoreCheckpoints`, so it does not track checkpointed blocks or checkpoints and omits `checkpointed` from the tips + * it reports. + */ + public async getL2Tips(): Promise { const status = await this.merkleTreeDb.getStatusSummary(); const unfinalizedBlockHashPromise = this.getL2BlockHash(status.unfinalizedBlockNumber); const finalizedBlockHashPromise = this.getL2BlockHash(status.finalizedBlockNumber); @@ -281,30 +282,13 @@ export class ServerWorldStateSynchronizer finalizedBlockHashPromise, provenBlockHashPromise, ]); - const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! }; - - // World state doesn't track checkpointed blocks or checkpoints themselves. - // but we use a block stream so we need to provide 'local' L2Tips. - // We configure the block stream to ignore checkpoints and set checkpoint values to genesis here. - const genesisCheckpointHeaderHash = GENESIS_CHECKPOINT_HEADER_HASH.toString(); - const initialBlockHash = (await this.merkleTreeCommitted.getInitialHeader().hash()).toString(); return { - proposed: latestBlockId, - checkpointed: { - block: { number: BlockNumber.ZERO, hash: initialBlockHash }, - checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash }, - }, - proposedCheckpoint: { - block: { number: BlockNumber.ZERO, hash: initialBlockHash }, - checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash }, - }, + proposed: { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash }, finalized: { - block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash ?? '' }, - checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash }, + block: { number: status.finalizedBlockNumber, hash: finalizedBlockHash }, }, proven: { - block: { number: provenBlockNumber, hash: provenBlockHash ?? '' }, - checkpoint: { number: INITIAL_CHECKPOINT_NUMBER, hash: genesisCheckpointHeaderHash }, + block: { number: provenBlockNumber, hash: provenBlockHash }, }, }; } diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index a1c1b9d3dcd8..3748bd51c824 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -280,6 +280,7 @@ describe('world-state integration', () => { synchronizer.handleBlockStreamEvent({ type: 'chain-finalized', block: { number: backwardsFinalized, hash: '' }, + checkpoint: { number: CheckpointNumber(1), hash: new Fr(1).toString() }, }), ).resolves.not.toThrow();