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 c6344662f839..a31f879a901d 100644 --- a/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md +++ b/docs/docs-developers/docs/tutorials/testing_governance_rollup_upgrade.md @@ -89,7 +89,6 @@ export AZTEC_EPOCH_DURATION=16 export AZTEC_TARGET_COMMITTEE_SIZE=48 export AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=2 export AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=2 -export AZTEC_INBOX_LAG=2 export AZTEC_PROOF_SUBMISSION_EPOCHS=2 export AZTEC_LOCAL_EJECTION_THRESHOLD=0 export AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS=1 diff --git a/docs/docs-operate/operators/reference/node-api-reference.md b/docs/docs-operate/operators/reference/node-api-reference.md index 084abb36b32b..e5264253cb1f 100644 --- a/docs/docs-operate/operators/reference/node-api-reference.md +++ b/docs/docs-operate/operators/reference/node-api-reference.md @@ -635,22 +635,24 @@ curl -X POST http://localhost:8080 \ -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageMembershipWitness","params":["latest","0x1234..."],"id":1}' ``` -### aztec_getL1ToL2MessageCheckpoint +### aztec_getL1ToL2MessageIndex -Returns the L2 checkpoint number in which this L1 to L2 message becomes available, or undefined if not found. +Returns the compact leaf index assigned to this L1 to L2 message as soon as the node has ingested it from L1, +before any L2 block consumes it. Returns undefined if the node has not yet seen the message. The message becomes +consumable once a block's L1-to-L2 message tree grows past this index. **Parameters**: 1. `l1ToL2Message` - `Fr` -**Returns**: `number | undefined` +**Returns**: `bigint | undefined` **Example**: ```bash curl -X POST http://localhost:8080 \ -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageCheckpoint","params":["0x1234..."],"id":1}' + -d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageIndex","params":["0x1234..."],"id":1}' ``` ### aztec_getL2ToL1MembershipWitness diff --git a/docs/examples/ts/aave_bridge/index.ts b/docs/examples/ts/aave_bridge/index.ts index d274a5726ea4..725332ac4bca 100644 --- a/docs/examples/ts/aave_bridge/index.ts +++ b/docs/examples/ts/aave_bridge/index.ts @@ -349,7 +349,6 @@ const INBOX_ABI = [ inputs: [ { name: "index", type: "uint256", indexed: false }, { name: "hash", type: "bytes32", indexed: true }, - { name: "rollingHash", type: "bytes16", indexed: false }, { name: "inboxRollingHash", type: "bytes32", indexed: false }, { name: "bucketSeq", type: "uint256", indexed: false }, ], diff --git a/docs/examples/ts/example_swap/index.ts b/docs/examples/ts/example_swap/index.ts index f12f638034a2..87b75160f49d 100644 --- a/docs/examples/ts/example_swap/index.ts +++ b/docs/examples/ts/example_swap/index.ts @@ -261,7 +261,6 @@ const INBOX_ABI = [ inputs: [ { name: "index", type: "uint256", indexed: false }, { name: "hash", type: "bytes32", indexed: true }, - { name: "rollingHash", type: "bytes16", indexed: false }, { name: "inboxRollingHash", type: "bytes32", indexed: false }, { name: "bucketSeq", type: "uint256", indexed: false }, ], diff --git a/docs/examples/ts/token_bridge/index.ts b/docs/examples/ts/token_bridge/index.ts index aa5ea9c23de9..ee68871013a2 100644 --- a/docs/examples/ts/token_bridge/index.ts +++ b/docs/examples/ts/token_bridge/index.ts @@ -167,7 +167,6 @@ const INBOX_ABI = [ inputs: [ { name: "index", type: "uint256", indexed: false }, { name: "hash", type: "bytes32", indexed: true }, - { name: "rollingHash", type: "bytes16", indexed: false }, { name: "inboxRollingHash", type: "bytes32", indexed: false }, { name: "bucketSeq", type: "uint256", indexed: false }, ], 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 73ad56bbcdf1..3a4c1b864f00 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 @@ -559,7 +559,7 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[] namespace: 'aztec', methods: [ 'getL1ToL2MessageMembershipWitness', - 'getL1ToL2MessageCheckpoint', + 'getL1ToL2MessageIndex', 'getL2ToL1MembershipWitness', 'getL2ToL1Messages', ], diff --git a/l1-contracts/scripts/network-defaults.json b/l1-contracts/scripts/network-defaults.json index 3d04bcf9ab46..96bc9ce58497 100644 --- a/l1-contracts/scripts/network-defaults.json +++ b/l1-contracts/scripts/network-defaults.json @@ -14,7 +14,6 @@ "AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN": 48, "AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT": 2, "AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE": 48, - "AZTEC_INBOX_LAG": 1, "AZTEC_PROOF_SUBMISSION_EPOCHS": 1, "AZTEC_MANA_TARGET": 100000000, "AZTEC_PROVING_COST_PER_MANA": 100, diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index db5fa4a34e85..a6451fb20631 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -27,7 +27,7 @@ struct PublicInputArgs { bytes32 previousArchive; bytes32 endArchive; bytes32 outHash; - // Inbox rolling-hash chain segment consumed across the proven epoch (AZIP-22 Fast Inbox). Both boundaries are + // Inbox rolling-hash chain segment consumed across the proven epoch. Both boundaries are // anchored at proof submission against the rolling hashes recorded at propose for checkpoints start - 1 and end. bytes32 previousInboxRollingHash; bytes32 endInboxRollingHash; diff --git a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol index 6c99ea496b84..11815bb79877 100644 --- a/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol +++ b/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol @@ -16,11 +16,6 @@ uint256 constant MAX_MSGS_PER_BUCKET = 256; */ interface IInbox { struct InboxState { - // Legacy 128-bit keccak rolling hash of all messages inserted into the inbox. Consumed only by the - // node for message sync and L1-reorg detection. - // TODO: remove once the node relies on the full-width consensus rolling hash tracked in the buckets - // instead (AZIP-22 Fast Inbox). - bytes16 rollingHash; // Cumulative number of messages inserted into the inbox. Useful for synching the node faster as it can // more easily figure out if it can just skip looking for events for a time period. uint64 totalMessagesInserted; @@ -50,13 +45,10 @@ interface IInbox { * @notice Emitted when a message is sent * @param index - The compact cumulative index of the message in the Inbox insertion order * @param hash - The hash of the message - * @param rollingHash - The legacy 128-bit rolling hash of all messages inserted into the inbox * @param inboxRollingHash - The consensus rolling hash (truncated sha256 chain) after this message * @param bucketSeq - The sequence number of the bucket this message was absorbed into */ - event MessageSent( - uint256 index, bytes32 indexed hash, bytes16 rollingHash, bytes32 inboxRollingHash, uint256 bucketSeq - ); + event MessageSent(uint256 index, bytes32 indexed hash, bytes32 inboxRollingHash, uint256 bucketSeq); // docs:start:send_l1_to_l2_message /** diff --git a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol index 877295b021b8..efb031bcbe9b 100644 --- a/l1-contracts/src/core/libraries/rollup/ProposeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/ProposeLib.sol @@ -401,8 +401,7 @@ library ProposeLib { /** * @notice Validates a checkpoint's Inbox consumption against the streaming inbox buckets and returns how - * far consumption has reached. Called from propose() as the enforced consumption path (AZIP-22 Fast - * Inbox). + * far consumption has reached. Called from propose() as the enforced consumption path. * * @dev Read-only; performs no Inbox write. Checks, in order: * 1. The checkpoint header's `inboxRollingHash` must equal the rolling hash snapshotted in the Inbox diff --git a/l1-contracts/src/core/messagebridge/Inbox.sol b/l1-contracts/src/core/messagebridge/Inbox.sol index 183164869dcd..fa1b0f5067e2 100644 --- a/l1-contracts/src/core/messagebridge/Inbox.sol +++ b/l1-contracts/src/core/messagebridge/Inbox.sol @@ -37,13 +37,8 @@ contract Inbox is IInbox { uint256 public immutable BUCKET_RING_SIZE; - // Legacy 128-bit keccak rolling hash over every inserted message leaf. Consumed only by the node's - // message sync and L1-reorg detection; retained until those switch to the full-width consensus rolling - // hash tracked in the buckets (AZIP-22 Fast Inbox). - bytes16 internal messagesRollingHash; - // Ring of rolling-hash buckets, keyed by `bucketSeq % BUCKET_RING_SIZE`. Consumed by the streaming inbox - // checks at `propose` (AZIP-22 Fast Inbox). + // checks at `propose`. mapping(uint256 ringIndex => InboxBucket bucket) internal buckets; uint64 internal currentBucketSeq; @@ -106,11 +101,9 @@ contract Inbox is IInbox { bytes32 leaf = message.sha256ToField(); - messagesRollingHash = bytes16(keccak256(abi.encodePacked(messagesRollingHash, leaf))); - (uint64 bucketSeq, bytes32 inboxRollingHash) = _absorbIntoBucket(leaf); - emit MessageSent(index, leaf, messagesRollingHash, inboxRollingHash, bucketSeq); + emit MessageSent(index, leaf, inboxRollingHash, bucketSeq); return (leaf, index); } @@ -120,7 +113,7 @@ contract Inbox is IInbox { } function getState() external view override(IInbox) returns (InboxState memory) { - return InboxState({rollingHash: messagesRollingHash, totalMessagesInserted: _totalMessagesInserted()}); + return InboxState({totalMessagesInserted: _totalMessagesInserted()}); } function getTotalMessagesInserted() external view override(IInbox) returns (uint64) { diff --git a/l1-contracts/test/Inbox.t.sol b/l1-contracts/test/Inbox.t.sol index 293a1e28c78e..d9661b10db02 100644 --- a/l1-contracts/test/Inbox.t.sol +++ b/l1-contracts/test/Inbox.t.sol @@ -66,11 +66,10 @@ contract InboxTest is Test { DataStructures.L1ToL2Msg memory message = _boundMessage(_message, globalLeafIndex); bytes32 leaf = message.sha256ToField(); - bytes16 expectedRollingHash = bytes16(keccak256(abi.encodePacked(stateBefore.rollingHash, leaf))); bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf); vm.expectEmit(true, true, true, true); // event we expect - emit IInbox.MessageSent(globalLeafIndex, leaf, expectedRollingHash, expectedInboxRollingHash, 1); + emit IInbox.MessageSent(globalLeafIndex, leaf, expectedInboxRollingHash, 1); // event we will get (bytes32 insertedLeaf, uint256 insertedIndex) = inbox.sendL2Message(message.recipient, message.content, message.secretHash); @@ -80,7 +79,6 @@ contract InboxTest is Test { Inbox.InboxState memory stateAfter = inbox.getState(); assertEq(stateBefore.totalMessagesInserted + 1, stateAfter.totalMessagesInserted); - assertEq(expectedRollingHash, stateAfter.rollingHash); } function testSendDuplicateL2Messages() public { diff --git a/l1-contracts/test/InboxBuckets.t.sol b/l1-contracts/test/InboxBuckets.t.sol index 3be204a67f94..86037e2f86b4 100644 --- a/l1-contracts/test/InboxBuckets.t.sol +++ b/l1-contracts/test/InboxBuckets.t.sol @@ -132,11 +132,10 @@ contract InboxBucketsTest is Test { index: inbox.getState().totalMessagesInserted }); bytes32 leaf = Hash.sha256ToField(message); - bytes16 legacyHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, leaf))); bytes32 inboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), leaf); vm.expectEmit(true, true, true, true, address(inbox)); - emit IInbox.MessageSent(message.index, leaf, legacyHash, inboxRollingHash, 1); + emit IInbox.MessageSent(message.index, leaf, inboxRollingHash, 1); inbox.sendL2Message(recipient, content, secretHash); } diff --git a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol index 25da1722339b..0c17e677d395 100644 --- a/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol +++ b/l1-contracts/test/fee_portal/depositToAztecPublic.t.sol @@ -63,7 +63,7 @@ contract DepositToAztecPublic is Test { uint256 amount = 100 ether; Inbox inbox = Inbox(address(Rollup(address(registry.getCanonicalRollup())).getInbox())); - // Compact cumulative index (AZIP-22 Fast Inbox): the first message against a fresh Inbox has index 0. + // Compact cumulative index: the first message against a fresh Inbox has index 0. uint256 expectedIndex = 0; // The purpose of including the function selector is to make the message unique to that specific call. Note that @@ -86,10 +86,9 @@ contract DepositToAztecPublic is Test { assertEq(inbox.getTotalMessagesInserted(), 0); - bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedKey))); bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedKey); vm.expectEmit(true, true, true, true, address(inbox)); - emit IInbox.MessageSent(expectedIndex, expectedKey, expectedHash, expectedInboxRollingHash, 1); + emit IInbox.MessageSent(expectedIndex, expectedKey, expectedInboxRollingHash, 1); vm.expectEmit(true, true, true, true, address(feeJuicePortal)); emit IFeeJuicePortal.DepositToAztecPublic(to, amount, secretHash, expectedKey, expectedIndex); diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index be63d4995778..a91f111003a5 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -120,12 +120,11 @@ contract TokenPortalTest is Test { DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedMintPrivateL1ToL2Message(expectedIndex); bytes32 expectedLeaf = expectedMessage.sha256ToField(); - bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedLeaf))); bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedLeaf); // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect - emit IInbox.MessageSent(expectedIndex, expectedLeaf, expectedHash, expectedInboxRollingHash, 1); + emit IInbox.MessageSent(expectedIndex, expectedLeaf, expectedInboxRollingHash, 1); // event we will get // Perform op @@ -148,13 +147,12 @@ contract TokenPortalTest is Test { uint256 expectedIndex = 0; DataStructures.L1ToL2Msg memory expectedMessage = _createExpectedMintPublicL1ToL2Message(expectedIndex); bytes32 expectedLeaf = expectedMessage.sha256ToField(); - bytes16 expectedHash = bytes16(keccak256(abi.encodePacked(inbox.getState().rollingHash, expectedLeaf))); bytes32 expectedInboxRollingHash = Hash.accumulateInboxRollingHash(bytes32(0), expectedLeaf); // Check the event was emitted vm.expectEmit(true, true, true, true); // event we expect - emit IInbox.MessageSent(expectedIndex, expectedLeaf, expectedHash, expectedInboxRollingHash, 1); + emit IInbox.MessageSent(expectedIndex, expectedLeaf, expectedInboxRollingHash, 1); // Perform op (bytes32 leaf, uint256 index) = tokenPortal.depositToAztecPublic(to, amount, secretHashForL2MessageConsumption); diff --git a/l1-contracts/test/script/DeployAztecL1Contracts.t.sol b/l1-contracts/test/script/DeployAztecL1Contracts.t.sol index b450e0a778b4..cd98934b568f 100644 --- a/l1-contracts/test/script/DeployAztecL1Contracts.t.sol +++ b/l1-contracts/test/script/DeployAztecL1Contracts.t.sol @@ -30,7 +30,6 @@ contract DeployAztecL1ContractsTest is Test { // Timing config vm.setEnv("AZTEC_SLOT_DURATION", vm.toString(json.readUint(".AZTEC_SLOT_DURATION"))); vm.setEnv("AZTEC_EPOCH_DURATION", vm.toString(json.readUint(".AZTEC_EPOCH_DURATION"))); - vm.setEnv("AZTEC_INBOX_LAG", vm.toString(json.readUint(".AZTEC_INBOX_LAG"))); vm.setEnv("AZTEC_PROOF_SUBMISSION_EPOCHS", vm.toString(json.readUint(".AZTEC_PROOF_SUBMISSION_EPOCHS"))); // Validator config diff --git a/l1-contracts/test/script/DeployRollupForUpgrade.t.sol b/l1-contracts/test/script/DeployRollupForUpgrade.t.sol index 11714808a0d3..0303ceb639f3 100644 --- a/l1-contracts/test/script/DeployRollupForUpgrade.t.sol +++ b/l1-contracts/test/script/DeployRollupForUpgrade.t.sol @@ -41,7 +41,6 @@ contract DeployRollupForUpgradeTest is Test { // Timing config vm.setEnv("AZTEC_SLOT_DURATION", vm.toString(json.readUint(".AZTEC_SLOT_DURATION"))); vm.setEnv("AZTEC_EPOCH_DURATION", vm.toString(json.readUint(".AZTEC_EPOCH_DURATION"))); - vm.setEnv("AZTEC_INBOX_LAG", vm.toString(json.readUint(".AZTEC_INBOX_LAG"))); vm.setEnv("AZTEC_PROOF_SUBMISSION_EPOCHS", vm.toString(json.readUint(".AZTEC_PROOF_SUBMISSION_EPOCHS"))); // Validator config diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/components/checkpoint_rollup_public_inputs_composer.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/components/checkpoint_rollup_public_inputs_composer.nr index b1f4bf8df971..1e3be282df92 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/components/checkpoint_rollup_public_inputs_composer.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_root/components/checkpoint_rollup_public_inputs_composer.nr @@ -145,7 +145,7 @@ impl CheckpointRollupPublicInputsComposer { last_archive_root: merged_rollup.previous_archive.root, block_headers_hash: merged_rollup.block_headers_hash, blobs_hash: self.blobs_hash, - // Sourced from the inbox parity proof: the checkpoint's only inbox commitment (AZIP-22 Fast Inbox). + // Sourced from the inbox parity proof: the checkpoint's only inbox commitment. inbox_rolling_hash: self.parity.end_rolling_hash, epoch_out_hash, slot_number: constants.slot_number, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/checkpoint_header.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/checkpoint_header.nr index ca150f0d819f..2dfb0cd1ba3d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/checkpoint_header.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/checkpoint_header.nr @@ -14,8 +14,8 @@ pub struct CheckpointHeader { pub last_archive_root: Field, pub block_headers_hash: Field, pub blobs_hash: Field, - // Inbox rolling-hash chain value after consuming all L1-to-L2 messages bundled into this checkpoint (AZIP-22 Fast - // Inbox): the truncated-to-field sha256 chain the L1 Inbox accumulates. This is the checkpoint's only inbox + // Inbox rolling-hash chain value after consuming all L1-to-L2 messages bundled into this checkpoint: + // the truncated-to-field sha256 chain the L1 Inbox accumulates. This is the checkpoint's only inbox // commitment. pub inbox_rolling_hash: Field, // The root of the epoch out hash balanced tree. The out hash of the first checkpoint in the epoch is inserted at diff --git a/spartan/environments/bench-10tps.env b/spartan/environments/bench-10tps.env index 3e7899fe1916..fd3222ca3961 100644 --- a/spartan/environments/bench-10tps.env +++ b/spartan/environments/bench-10tps.env @@ -18,7 +18,6 @@ AZTEC_SLOT_DURATION=72 AZTEC_PROOF_SUBMISSION_EPOCHS=2 AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=1 AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=1 -AZTEC_INBOX_LAG=2 # 2B mana target - good for about ~800 txs at 2.5M mana each AZTEC_MANA_TARGET=2000000000 diff --git a/spartan/environments/bench-inclusion-sweep.env b/spartan/environments/bench-inclusion-sweep.env index 393dadcbf755..fcb2a94fc18f 100644 --- a/spartan/environments/bench-inclusion-sweep.env +++ b/spartan/environments/bench-inclusion-sweep.env @@ -19,7 +19,6 @@ AZTEC_SLOT_DURATION=72 AZTEC_PROOF_SUBMISSION_EPOCHS=100 AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=1 AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=1 -AZTEC_INBOX_LAG=2 AZTEC_MANA_TARGET=2000000000 diff --git a/spartan/environments/network-defaults.yml b/spartan/environments/network-defaults.yml index 6b6bb4bf127b..a412219b9a34 100644 --- a/spartan/environments/network-defaults.yml +++ b/spartan/environments/network-defaults.yml @@ -59,8 +59,6 @@ l1-contracts: &l1-contracts-defaults #--------------------------------------------------------------------------- # Proof & Block Configuration #--------------------------------------------------------------------------- - # Checkpoints to lag in inbox (prevents sequencer DOS attacks). - AZTEC_INBOX_LAG: 2 # Epochs after end that proofs are still accepted. AZTEC_PROOF_SUBMISSION_EPOCHS: 1 # Target mana consumption per block. diff --git a/spartan/environments/next-net.env b/spartan/environments/next-net.env index de59eced52a6..4a6ba4281702 100644 --- a/spartan/environments/next-net.env +++ b/spartan/environments/next-net.env @@ -39,7 +39,6 @@ SEQ_MAX_TX_PER_CHECKPOINT=12 SEQ_BUILD_CHECKPOINT_IF_EMPTY=true SEQ_BLOCK_DURATION_MS=5500 -AZTEC_INBOX_LAG=2 VALIDATOR_REPLICAS=2 VALIDATORS_PER_NODE=24 diff --git a/spartan/environments/next-scenario.env b/spartan/environments/next-scenario.env index 1f5adb8ebf0f..b7b198419a04 100644 --- a/spartan/environments/next-scenario.env +++ b/spartan/environments/next-scenario.env @@ -7,7 +7,6 @@ DESTROY_ETH_DEVNET=true CREATE_ETH_DEVNET=true AZTEC_EPOCH_DURATION=32 AZTEC_SLOT_DURATION=36 -AZTEC_INBOX_LAG=2 ETHEREUM_CHAIN_ID=1337 LABS_INFRA_MNEMONIC="test test test test test test test test test test test junk" FUNDING_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" diff --git a/spartan/environments/staging-internal.env b/spartan/environments/staging-internal.env index 69da3a5a1fd6..c3568e106c9c 100644 --- a/spartan/environments/staging-internal.env +++ b/spartan/environments/staging-internal.env @@ -36,7 +36,6 @@ SPONSORED_FPC=false # Rollup AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=2 AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=2 -AZTEC_INBOX_LAG=2 AZTEC_MANA_TARGET=75000000 AZTEC_PROVING_COST_PER_MANA=12500000 diff --git a/spartan/environments/staging-public.env b/spartan/environments/staging-public.env index 7749f4f3533a..4da560112d34 100644 --- a/spartan/environments/staging-public.env +++ b/spartan/environments/staging-public.env @@ -36,7 +36,6 @@ SPONSORED_FPC=false # Rollup AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET=2 AZTEC_LAG_IN_EPOCHS_FOR_RANDAO=2 -AZTEC_INBOX_LAG=2 AZTEC_MANA_TARGET=75000000 AZTEC_PROVING_COST_PER_MANA=12500000 diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index 5b574dfd8635..1165fc41a471 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -460,7 +460,6 @@ AZTEC_LAG_IN_EPOCHS_FOR_RANDAO = ${AZTEC_LAG_IN_EPOCHS_FOR_RANDAO:-null} AZTEC_SLOT_DURATION = ${AZTEC_SLOT_DURATION:-null} AZTEC_EPOCH_DURATION = ${AZTEC_EPOCH_DURATION:-null} AZTEC_TARGET_COMMITTEE_SIZE = ${AZTEC_TARGET_COMMITTEE_SIZE:-null} -AZTEC_INBOX_LAG = ${AZTEC_INBOX_LAG:-null} AZTEC_PROOF_SUBMISSION_EPOCHS = ${AZTEC_PROOF_SUBMISSION_EPOCHS:-null} AZTEC_ACTIVATION_THRESHOLD = ${AZTEC_ACTIVATION_THRESHOLD:-null} AZTEC_EJECTION_THRESHOLD = ${AZTEC_EJECTION_THRESHOLD:-null} diff --git a/spartan/terraform/deploy-rollup-contracts/main.tf b/spartan/terraform/deploy-rollup-contracts/main.tf index 690ef063b2e3..d1dd567cad99 100644 --- a/spartan/terraform/deploy-rollup-contracts/main.tf +++ b/spartan/terraform/deploy-rollup-contracts/main.tf @@ -43,7 +43,6 @@ locals { AZTEC_SLOT_DURATION = var.AZTEC_SLOT_DURATION AZTEC_EPOCH_DURATION = var.AZTEC_EPOCH_DURATION AZTEC_TARGET_COMMITTEE_SIZE = var.AZTEC_TARGET_COMMITTEE_SIZE - AZTEC_INBOX_LAG = var.AZTEC_INBOX_LAG AZTEC_PROOF_SUBMISSION_EPOCHS = var.AZTEC_PROOF_SUBMISSION_EPOCHS AZTEC_ACTIVATION_THRESHOLD = var.AZTEC_ACTIVATION_THRESHOLD AZTEC_EJECTION_THRESHOLD = var.AZTEC_EJECTION_THRESHOLD diff --git a/spartan/terraform/deploy-rollup-contracts/variables.tf b/spartan/terraform/deploy-rollup-contracts/variables.tf index f0031b7768ed..c40b8a2ef02a 100644 --- a/spartan/terraform/deploy-rollup-contracts/variables.tf +++ b/spartan/terraform/deploy-rollup-contracts/variables.tf @@ -86,12 +86,6 @@ variable "AZTEC_PROOF_SUBMISSION_EPOCHS" { nullable = true } -variable "AZTEC_INBOX_LAG" { - description = "Checkpoints to lag in inbox (prevents sequencer DOS attacks)" - type = string - nullable = true -} - variable "AZTEC_ACTIVATION_THRESHOLD" { description = "Aztec activation threshold" type = string diff --git a/yarn-project/archiver/README.md b/yarn-project/archiver/README.md index 959f16f819dc..29735e31f2ea 100644 --- a/yarn-project/archiver/README.md +++ b/yarn-project/archiver/README.md @@ -44,9 +44,9 @@ Two independent syncpoints track progress on L1: ### L1-to-L2 Messages -Messages are synced from the Inbox contract. The sync compares local state (message count and rolling hash) against the Inbox contract state on L1, downloads any missing messages, and verifies consistency afterwards. On success, the syncpoint advances to the current L1 block. On failure (L1 reorg or inconsistency), the syncpoint rolls back to the last known-good message and the operation retries (up to 3 times within the same sync iteration). +Messages are synced from the Inbox contract. The sync compares local state (message count and consensus rolling hash) against the Inbox's current rolling-hash bucket on L1, downloads any missing messages, and verifies consistency afterwards. On success, the syncpoint advances to the current L1 block. On failure (L1 reorg or inconsistency), the syncpoint rolls back to the last known-good message and the operation retries (up to 3 times within the same sync iteration). -1. Query Inbox state at the current L1 block (message count + rolling hash) +1. Query the Inbox's current bucket at the current L1 block (cumulative message count + consensus rolling hash) 2. Compare local state against remote 3. If they match, advance syncpoint and return 4. If mismatch, fetch `MessageSent` events in batches and store them @@ -55,7 +55,7 @@ Messages are synced from the Inbox contract. The sync compares local state (mess - If still mismatched (e.g., messages missed due to a concurrent L1 reorg), rollback and retry 6. On success, advance the syncpoint -The syncpoint and the `inboxTreeInProgress` marker (which tracks which checkpoint's messages are currently being filled on L1) are updated atomically. The marker is only advanced after messages are stored, so concurrent reads don't see an unsealed checkpoint as readable before its messages are available. +Messages are stored with compact (unpadded) global indices matching the Inbox's insertion order, and each carries the consensus rolling hash (a truncated sha256 chain) and the sequence of the Inbox bucket it was absorbed into (AZIP-22 Fast Inbox). Bucket snapshots let the sequencer and validator resolve message bundles per block. ### Checkpoints @@ -70,13 +70,12 @@ Checkpoints are synced from the Rollup contract via `handleCheckpoints()`: - Verify archive matches (checkpoint still in chain) - Validate attestations (2/3 + 1 committee signatures required) - Skip invalid checkpoints (see "Invalid Checkpoints" in Edge Cases) - - Verify `inHash` matches expected value (see below) - Store valid checkpoints with their blocks 6. Update proven checkpoint again (may have advanced after storing new checkpoints) 7. Handle epoch prune if applicable 8. Check for checkpoints behind syncpoint (L1 reorg case) -The `inHash` is a hash of all L1-to-L2 messages consumed by a checkpoint. The archiver computes the expected `inHash` from locally stored messages and compares it against the checkpoint header. A mismatch indicates a bug (messages out of sync with checkpoints) and causes a fatal error. +L1 enforces at propose time that a checkpoint header's consensus rolling hash matches the Inbox bucket the checkpoint consumes through (AZIP-22 Fast Inbox), so the archiver does not re-derive or cross-check a per-checkpoint message hash while syncing. The `blocksSynchedTo` syncpoint is updated: - When checkpoints are stored: set to the L1 block of the last stored checkpoint diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index 3cfffde67ee8..05120d1a9798 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -441,22 +441,22 @@ describe('Archiver Sync', () => { logger.warn('Initial sync'); await archiver.syncImmediate(); - expect(inboxContract.getState).toHaveBeenCalledTimes(1); + expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1); expect(rollupContract.status).toHaveBeenCalledTimes(1); - inboxContract.getState.mockClear(); + inboxContract.getCurrentBucket.mockClear(); rollupContract.status.mockClear(); // We sync again, but since chain didn't move, no new calls should be expected logger.warn('Sync with no L1 advancement'); await archiver.syncImmediate(); - expect(inboxContract.getState).toHaveBeenCalledTimes(0); + expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(0); expect(rollupContract.status).toHaveBeenCalledTimes(0); // Advance the chain and we should see calls again fake.setL1BlockNumber(150n); logger.warn('Sync after L1 advancement'); await archiver.syncImmediate(); - expect(inboxContract.getState).toHaveBeenCalledTimes(1); + expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1); expect(rollupContract.status).toHaveBeenCalledTimes(1); }); diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index 501d624da8eb..1ba6df8b6d8d 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -764,14 +764,14 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra `Removing checkpoints after checkpoint ${targetCheckpointNumber} (target block ${targetL2BlockNumber})`, ); await this.updater.removeCheckpointsAfter(targetCheckpointNumber); - this.log.info(`Rolling back L1 to L2 messages to checkpoint ${targetCheckpointNumber}`); - await this.stores.messages.rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber); + this.log.info(`Rolling back L1 to L2 messages inserted after L1 block ${targetL1BlockNumber}`); + await this.stores.messages.rollbackL1ToL2MessagesAfterL1Block(targetL1BlockNumber); this.log.info(`Setting L1 syncpoints to ${targetL1BlockNumber}`); await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber); - await this.stores.messages.setMessageSyncState( - { l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash }, - undefined, - ); + await this.stores.messages.setMessageSyncState({ + l1BlockNumber: targetL1BlockNumber, + l1BlockHash: targetL1BlockHash, + }); if (targetL2BlockNumber < currentProvenBlock) { this.log.info(`Rolling back proven L2 checkpoint to ${targetCheckpointNumber}`); await this.updater.setProvenCheckpointNumber(targetCheckpointNumber); diff --git a/yarn-project/archiver/src/errors.ts b/yarn-project/archiver/src/errors.ts index 29f4de4e84b6..60661d9ea296 100644 --- a/yarn-project/archiver/src/errors.ts +++ b/yarn-project/archiver/src/errors.ts @@ -102,20 +102,6 @@ export class BlockAlreadyCheckpointedError extends Error { } } -/** Thrown when L1 to L2 messages are requested for a checkpoint whose message tree hasn't been sealed yet. */ -export class L1ToL2MessagesNotReadyError extends Error { - constructor( - public readonly checkpointNumber: number, - public readonly inboxTreeInProgress: bigint, - ) { - super( - `Cannot get L1 to L2 messages for checkpoint ${checkpointNumber}: ` + - `inbox tree in progress is ${inboxTreeInProgress}, messages not yet sealed`, - ); - this.name = 'L1ToL2MessagesNotReadyError'; - } -} - /** * Thrown when a query names an Inbox bucket this archiver has not synced. Distinguishes "not synced yet, retry once * L1 sync catches up" from a genuinely empty result. diff --git a/yarn-project/archiver/src/l1/data_retrieval.ts b/yarn-project/archiver/src/l1/data_retrieval.ts index 358648e7d306..7f0602f06fb4 100644 --- a/yarn-project/archiver/src/l1/data_retrieval.ts +++ b/yarn-project/archiver/src/l1/data_retrieval.ts @@ -24,7 +24,6 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { RollupAbi } from '@aztec/l1-artifacts'; import { Body, CommitteeAttestation, L2Block } from '@aztec/stdlib/block'; import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; -import { InboxLeaf } from '@aztec/stdlib/messaging'; import { Proof } from '@aztec/stdlib/proofs'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; @@ -364,7 +363,7 @@ export async function retrieveL1ToL2Message( * @param inbox - The inbox contract wrapper. * @param searchStartBlock - The block number to use for starting the search. * @param searchEndBlock - The highest block number that we should search up to. - * @returns An array of InboxLeaf and next eth block to search from. + * @returns The L1 to L2 messages retrieved from the Inbox, as an array of InboxMessage. */ export async function retrieveL1ToL2Messages( inbox: InboxContract, @@ -392,10 +391,6 @@ function mapLogInboxMessage(log: MessageSentLog): InboxMessage { leaf: log.args.leaf, l1BlockNumber: log.l1BlockNumber, l1BlockHash: log.l1BlockHash, - // The Inbox no longer emits a checkpoint number (AZIP-22 Fast Inbox). Derive it from the compact index for the - // legacy per-checkpoint message store, which the node still keeps until it drops the per-checkpoint flow. - checkpointNumber: InboxLeaf.checkpointNumberFromIndex(log.args.index), - rollingHash: log.args.rollingHash, inboxRollingHash: log.args.inboxRollingHash, bucketSeq: log.args.bucketSeq, bucketTimestamp: log.l1BlockTimestamp, diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index 9d2256c7baa6..9e407d9e6765 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -316,10 +316,6 @@ export abstract class ArchiverDataSourceBase return this.stores.functionNames.register(signatures); } - public getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise { - return this.stores.messages.getL1ToL2Messages(checkpointNumber); - } - public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { return this.stores.messages.getL1ToL2MessageIndex(l1ToL2Message); } diff --git a/yarn-project/archiver/src/modules/l1_synchronizer.ts b/yarn-project/archiver/src/modules/l1_synchronizer.ts index ba5876f77500..be1bbf0b2d17 100644 --- a/yarn-project/archiver/src/modules/l1_synchronizer.ts +++ b/yarn-project/archiver/src/modules/l1_synchronizer.ts @@ -1,13 +1,13 @@ import type { BlobClientInterface } from '@aztec/blob-client/client'; import { EpochCache } from '@aztec/epoch-cache'; -import { InboxContract, type InboxContractState, RollupContract } from '@aztec/ethereum/contracts'; +import { InboxContract, type InboxContractBucket, RollupContract } from '@aztec/ethereum/contracts'; import type { L1BlockId } from '@aztec/ethereum/l1-types'; import { getFinalizedL1Block } from '@aztec/ethereum/queries'; import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types'; import { asyncPool } from '@aztec/foundation/async-pool'; import { maxBigint } from '@aztec/foundation/bigint'; import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { compactArray, partition, pick } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -430,16 +430,14 @@ export class ArchiverL1Synchronizer implements Traceable { return true; } - // Compare local message store state with the remote. If they match, we just advance the match pointer. - const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber }); + // Compare local message store state with the remote. If they match, we just advance the match pointer. The + // remote state is the Inbox's current bucket: its cumulative total and consensus rolling + // hash are the Inbox's live chain position. + const remoteBucket = await this.inbox.getCurrentBucket({ blockNumber: currentL1BlockNumber }); const localLastMessage = await this.stores.messages.getLastMessage(); - if (await this.localStateMatches(localLastMessage, remoteMessagesState)) { + if (await this.localStateMatches(localLastMessage, remoteBucket)) { this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`); - await this.stores.messages.setMessageSyncState( - currentL1Block, - remoteMessagesState.treeInProgress, - finalizedL1Block, - ); + await this.stores.messages.setMessageSyncState(currentL1Block, finalizedL1Block); return true; } @@ -455,7 +453,7 @@ export class ArchiverL1Synchronizer implements Traceable { `Failed to store L1 to L2 messages retrieved from L1: ${error.message}. Rolling back syncpoint to retry.`, { inboxMessage: error.inboxMessage }, ); - await this.rollbackL1ToL2Messages(remoteMessagesState); + await this.rollbackL1ToL2Messages(remoteBucket); return false; } throw error; @@ -465,32 +463,28 @@ export class ArchiverL1Synchronizer implements Traceable { // we'd notice by comparing our local state with the remote one again, and seeing they don't match even after // our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry. const localLastMessageAfterSync = await this.stores.messages.getLastMessage(); - if (!(await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState))) { + if (!(await this.localStateMatches(localLastMessageAfterSync, remoteBucket))) { this.log.warn( `Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`, - { localLastMessageAfterSync, remoteMessagesState }, + { localLastMessageAfterSync, remoteBucket }, ); - await this.rollbackL1ToL2Messages(remoteMessagesState); + await this.rollbackL1ToL2Messages(remoteBucket); return false; } // Advance the syncpoint after a successful sync - await this.stores.messages.setMessageSyncState( - currentL1Block, - remoteMessagesState.treeInProgress, - finalizedL1Block, - ); + await this.stores.messages.setMessageSyncState(currentL1Block, finalizedL1Block); return true; } - /** Checks if the local rolling hash and message count matches the remote state */ - private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteState: InboxContractState) { + /** Checks if the local consensus rolling hash and message count match the remote Inbox current bucket. */ + private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteBucket: InboxContractBucket) { const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount(); - this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteState }); + this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteBucket }); return ( - remoteState.totalMessagesInserted === localMessageCount && - remoteState.messagesRollingHash.equals(localLastMessage?.rollingHash ?? Buffer16.ZERO) + remoteBucket.totalMsgCount === localMessageCount && + remoteBucket.rollingHash.equals(localLastMessage?.inboxRollingHash ?? Fr.ZERO) ); } @@ -521,10 +515,10 @@ export class ArchiverL1Synchronizer implements Traceable { } while (searchEndBlock < toL1Block); if (messageCount > 0) { - this.log.info( - `Retrieved ${messageCount} new L1 to L2 messages up to message with index ${lastMessage?.index} for checkpoint ${lastMessage?.checkpointNumber}`, - { lastMessage, messageCount }, - ); + this.log.info(`Retrieved ${messageCount} new L1 to L2 messages up to message with index ${lastMessage?.index}`, { + lastMessage, + messageCount, + }); } } @@ -532,8 +526,8 @@ export class ArchiverL1Synchronizer implements Traceable { * Rolls back local L1 to L2 messages to the last common message with L1, and updates the syncpoint to the L1 block of that message. * If no common message is found, rolls back all messages and sets the syncpoint to the start block. */ - private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise { - const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState; + private async rollbackL1ToL2Messages(remoteBucket: InboxContractBucket): Promise { + const remoteRollingHash = remoteBucket.rollingHash; const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block(); const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber; @@ -545,12 +539,12 @@ export class ArchiverL1Synchronizer implements Traceable { let messagesToDelete = 0; this.log.verbose(`Searching most recent common L1 to L2 message`); for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({ reverse: true })) { - const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState }; + const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteBucket }; // First check if the local message rolling hash matches the current rolling hash of the inbox contract, // which means we just need to rollback some local messages and we should be back in sync. This means there // was an L1 reorg that removed some of the messages we had, but no new messages were added compared. - if (localMsg.rollingHash.equals(remoteRollingHash)) { + if (localMsg.inboxRollingHash.equals(remoteRollingHash)) { this.log.info( `Found common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber} matching current remote state`, logCtx, @@ -573,7 +567,7 @@ export class ArchiverL1Synchronizer implements Traceable { // an archival rpc node, since the message could be from a long time ago if we're catching up with syncing. const remoteMsg = await retrieveL1ToL2Message(this.inbox, localMsg); logCtx.remoteMsg = remoteMsg; - if (remoteMsg && remoteMsg.rollingHash.equals(localMsg.rollingHash)) { + if (remoteMsg && remoteMsg.inboxRollingHash.equals(localMsg.inboxRollingHash)) { this.log.info( `Found most recent common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber}`, logCtx, @@ -615,10 +609,9 @@ export class ArchiverL1Synchronizer implements Traceable { : await this.getL1BlockHash(syncPointL1BlockNumber); const messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash }; - await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress); + await this.stores.messages.setMessageSyncState(messagesSyncPoint); this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, { ...messagesSyncPoint, - remoteTreeInProgress, }); return messagesSyncPoint; } diff --git a/yarn-project/archiver/src/store/data_stores.ts b/yarn-project/archiver/src/store/data_stores.ts index f463e06735ba..0db4c23fca7c 100644 --- a/yarn-project/archiver/src/store/data_stores.ts +++ b/yarn-project/archiver/src/store/data_stores.ts @@ -13,7 +13,7 @@ import { FunctionNamesCache } from './function_names_cache.js'; import { LogStore } from './log_store.js'; import { MessageStore } from './message_store.js'; -export const ARCHIVER_DB_VERSION = 8; +export const ARCHIVER_DB_VERSION = 9; /** * Represents the latest L1 block processed by the archiver for various objects in L2. diff --git a/yarn-project/archiver/src/store/message_store.test.ts b/yarn-project/archiver/src/store/message_store.test.ts index 440a8866a268..b60745673c09 100644 --- a/yarn-project/archiver/src/store/message_store.test.ts +++ b/yarn-project/archiver/src/store/message_store.test.ts @@ -1,5 +1,5 @@ import { CheckpointNumber } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Fr } from '@aztec/foundation/curves/bn254'; import { toArray } from '@aztec/foundation/iterable'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; @@ -8,7 +8,7 @@ import { updateInboxRollingHash } from '@aztec/stdlib/messaging'; import '@aztec/stdlib/testing/jest'; import { InboxBucketBoundaryNotSyncedError, InboxBucketNotSyncedError } from '../errors.js'; -import { type InboxMessage, updateRollingHash } from '../structs/inbox_message.js'; +import type { InboxMessage } from '../structs/inbox_message.js'; import { makeInboxMessage, makeInboxMessages, @@ -78,9 +78,9 @@ describe('MessageStore', () => { it('returns the L1 block set via setMessageSyncState', async () => { const l1BlockHash = Buffer32.random(); const l1BlockNumber = 10n; - await messageStore.setMessageSyncState({ l1BlockNumber, l1BlockHash }, 1n); + await messageStore.setMessageSyncState({ l1BlockNumber, l1BlockHash }); await messageStore.addL1ToL2MessageBuckets([ - makeInboxMessage(Buffer16.ZERO, { l1BlockNumber: 5n, l1BlockHash: Buffer32.random() }), + makeInboxMessage(Fr.ZERO, { l1BlockNumber: 5n, l1BlockHash: Buffer32.random() }), ]); await expect(getSynchPoint(blockStore, messageStore)).resolves.toEqual({ blocksSynchedTo: undefined, @@ -90,8 +90,6 @@ describe('MessageStore', () => { }); describe('L1 to L2 Messages', () => { - const initialCheckpointNumber = CheckpointNumber(13); - const checkMessages = async (msgs: InboxMessage[]) => { expect(await messageStore.getLastMessage()).toEqual(msgs.at(-1)); expect(await toArray(messageStore.iterateL1ToL2Messages())).toEqual(msgs); @@ -99,42 +97,48 @@ describe('MessageStore', () => { }; it('stores first message ever', async () => { - const msg = makeInboxMessage(Buffer16.ZERO, { index: 0n, checkpointNumber: CheckpointNumber(1) }); - await messageStore.addL1ToL2MessageBuckets([msg]); - - await checkMessages([msg]); - }); - - it('stores single message', async () => { - const msg = makeInboxMessage(Buffer16.ZERO, { checkpointNumber: CheckpointNumber(2) }); + const msg = makeInboxMessage(Fr.ZERO, { index: 0n }); await messageStore.addL1ToL2MessageBuckets([msg]); await checkMessages([msg]); }); - it('stores messages across different blocks', async () => { - const msgs = makeInboxMessages(5, { initialCheckpointNumber }); + it('stores and returns messages across different blocks', async () => { + const msgs = makeInboxMessages(5); await messageStore.addL1ToL2MessageBuckets(msgs); await checkMessages(msgs); }); it('stores the same messages again', async () => { - const msgs = makeInboxMessages(5, { initialCheckpointNumber }); + const msgs = makeInboxMessages(5); await messageStore.addL1ToL2MessageBuckets(msgs); await messageStore.addL1ToL2MessageBuckets(msgs.slice(2)); await checkMessages(msgs); }); - it('stores messages with block numbers larger than a byte', async () => { - const msgs = makeInboxMessages(5, { initialCheckpointNumber: CheckpointNumber(1000) }); + it('stores messages added in two chained batches', async () => { + const msgs1 = makeInboxMessages(3); + const msgs2 = makeInboxMessages(3, { + initialInboxHash: msgs1.at(-1)!.inboxRollingHash, + initialIndex: BigInt(msgs1.length), + }); + + await messageStore.addL1ToL2MessageBuckets(msgs1); + await messageStore.addL1ToL2MessageBuckets(msgs2); + + await checkMessages([...msgs1, ...msgs2]); + }); + + it('stores and returns messages with block numbers larger than a byte', async () => { + const msgs = makeInboxMessages(5, { overrideFn: (msg, i) => ({ ...msg, l1BlockNumber: BigInt(1000 + i) }) }); await messageStore.addL1ToL2MessageBuckets(msgs); await checkMessages(msgs); }); - it('stores multiple messages per block', async () => { + it('stores and returns multiple messages per block', async () => { const msgs = makeInboxMessagesWithFullBlocks(4); await messageStore.addL1ToL2MessageBuckets(msgs); @@ -142,7 +146,7 @@ describe('MessageStore', () => { }); it('stores messages in multiple operations', async () => { - const msgs = makeInboxMessages(20, { initialCheckpointNumber }); + const msgs = makeInboxMessages(20); await messageStore.addL1ToL2MessageBuckets(msgs.slice(0, 10)); await messageStore.addL1ToL2MessageBuckets(msgs.slice(10, 20)); @@ -150,7 +154,7 @@ describe('MessageStore', () => { }); it('iterates over messages from start index', async () => { - const msgs = makeInboxMessages(10, { initialCheckpointNumber }); + const msgs = makeInboxMessages(10); await messageStore.addL1ToL2MessageBuckets(msgs); const iterated = await toArray(messageStore.iterateL1ToL2Messages({ start: msgs[3].index })); @@ -158,7 +162,7 @@ describe('MessageStore', () => { }); it('iterates over messages in reverse', async () => { - const msgs = makeInboxMessages(10, { initialCheckpointNumber }); + const msgs = makeInboxMessages(10); await messageStore.addL1ToL2MessageBuckets(msgs); const iterated = await toArray(messageStore.iterateL1ToL2Messages({ reverse: true, end: msgs[3].index })); @@ -170,33 +174,47 @@ describe('MessageStore', () => { await expect(messageStore.addL1ToL2MessageBuckets(msgs)).rejects.toThrow(MessageStoreError); }); - it('throws if rolling hash is not correct', async () => { + it('throws if index is not contiguous with the previous message', async () => { const msgs = makeInboxMessages(5); - msgs[1].rollingHash = Buffer16.random(); + msgs.at(-1)!.index += 100n; await expect(messageStore.addL1ToL2MessageBuckets(msgs)).rejects.toThrow(MessageStoreError); }); - it('throws if rolling hash for first message is not correct', async () => { + it('throws if there is a gap in the indices', async () => { const msgs = makeInboxMessages(4); - msgs[2].rollingHash = Buffer16.random(); - await messageStore.addL1ToL2MessageBuckets(msgs.slice(0, CheckpointNumber(2))); + msgs[2].index++; + await expect(messageStore.addL1ToL2MessageBuckets(msgs)).rejects.toThrow(MessageStoreError); + }); + + it('throws if the first index of a batch does not follow the last stored message', async () => { + const msgs = makeInboxMessages(4); + await messageStore.addL1ToL2MessageBuckets(msgs.slice(0, 2)); + msgs[2].index++; await expect(messageStore.addL1ToL2MessageBuckets(msgs.slice(2, 4))).rejects.toThrow(MessageStoreError); }); - it('throws if index skips ahead', async () => { - const msgs = makeInboxMessages(5, { initialCheckpointNumber }); - msgs.at(-1)!.index += 100n; + it('throws if the consensus rolling hash is not correct', async () => { + const msgs = makeInboxMessages(5); + msgs[1].inboxRollingHash = Fr.random(); await expect(messageStore.addL1ToL2MessageBuckets(msgs)).rejects.toThrow(MessageStoreError); }); - it('throws if index does not follow previous one', async () => { - const msgs = makeInboxMessages(4, { initialCheckpointNumber }); - msgs[2].index++; - await expect(messageStore.addL1ToL2MessageBuckets(msgs)).rejects.toThrow(MessageStoreError); + it('removes messages inserted after a given L1 block', async () => { + // Two messages per L1 block: [100, 100, 101, 101, 102, 102]. + const msgs = makeInboxMessages(6, { + overrideFn: (msg, i) => ({ ...msg, l1BlockNumber: BigInt(100 + Math.floor(i / 2)) }), + }); + await messageStore.addL1ToL2MessageBuckets(msgs); + await checkMessages(msgs); + + await messageStore.rollbackL1ToL2MessagesAfterL1Block(101n); + + // Only the messages inserted at or before L1 block 101 survive. + await checkMessages(msgs.slice(0, 4)); }); it('removes messages starting with the given index', async () => { - const msgs = makeInboxMessagesWithFullBlocks(4, { initialCheckpointNumber: CheckpointNumber(1) }); + const msgs = makeInboxMessagesWithFullBlocks(4); await messageStore.addL1ToL2MessageBuckets(msgs); await messageStore.removeL1ToL2Messages(msgs[13].index); @@ -205,16 +223,16 @@ describe('MessageStore', () => { }); describe('Inbox buckets', () => { - // Builds `count` consecutive valid messages in a single checkpoint, then reassigns their bucket sequence and - // timestamp per the given per-message spec so we can exercise multi-message and rollover buckets. - const makeBucketedMessages = (spec: { seq: bigint; timestamp: bigint }[]): InboxMessage[] => { - const msgs = makeInboxMessages(spec.length, { - initialCheckpointNumber: CheckpointNumber(1), - messagesPerCheckpoint: spec.length, - }); + // Builds `count` consecutive valid messages, then reassigns their bucket sequence and timestamp per the given + // per-message spec so we can exercise multi-message and rollover buckets. + const makeBucketedMessages = ( + spec: { seq: bigint; timestamp: bigint; l1BlockNumber?: bigint }[], + ): InboxMessage[] => { + const msgs = makeInboxMessages(spec.length); msgs.forEach((msg, i) => { msg.bucketSeq = spec[i].seq; msg.bucketTimestamp = spec[i].timestamp; + msg.l1BlockNumber = spec[i].l1BlockNumber ?? msg.l1BlockNumber; }); return msgs; }; @@ -226,7 +244,6 @@ describe('MessageStore', () => { ...previous, leaf, index: previous.index + 1n, - rollingHash: updateRollingHash(previous.rollingHash, leaf), inboxRollingHash: updateInboxRollingHash(previous.inboxRollingHash, leaf), bucketSeq: bucket.seq, bucketTimestamp: bucket.timestamp, @@ -491,6 +508,27 @@ describe('MessageStore', () => { expect((await messageStore.getLatestInboxBucketAtOrBefore(150n))!.seq).toEqual(1n); }); + it('rolls back whole buckets past an L1 block', async () => { + // Bucket 1 fills L1 block 100, bucket 2 fills L1 block 101 and bucket 3 fills L1 block 102. + const msgs = makeBucketedMessages([ + { seq: 1n, timestamp: 100n, l1BlockNumber: 100n }, + { seq: 1n, timestamp: 100n, l1BlockNumber: 100n }, + { seq: 1n, timestamp: 100n, l1BlockNumber: 100n }, + { seq: 2n, timestamp: 200n, l1BlockNumber: 101n }, + { seq: 2n, timestamp: 200n, l1BlockNumber: 101n }, + { seq: 3n, timestamp: 300n, l1BlockNumber: 102n }, + ]); + await messageStore.addL1ToL2MessageBuckets(msgs); + + await messageStore.rollbackL1ToL2MessagesAfterL1Block(100n); + + expect(await toArray(messageStore.iterateL1ToL2Messages())).toEqual(msgs.slice(0, 3)); + expect(await messageStore.getInboxBucket(2n)).toBeUndefined(); + expect(await messageStore.getInboxBucket(3n)).toBeUndefined(); + expect(await messageStore.getInboxBucket(1n)).toMatchObject({ msgCount: 3, totalMsgCount: 3n }); + expect((await messageStore.getLatestInboxBucketAtOrBefore(300n))!.seq).toEqual(1n); + }); + it('clears all buckets when every message is removed', async () => { const msgs = makeBucketedMessages(threeBucketSpec); await messageStore.addL1ToL2MessageBuckets(msgs); diff --git a/yarn-project/archiver/src/store/message_store.ts b/yarn-project/archiver/src/store/message_store.ts index da9351708e4d..0590a060f4f2 100644 --- a/yarn-project/archiver/src/store/message_store.ts +++ b/yarn-project/archiver/src/store/message_store.ts @@ -1,6 +1,5 @@ import type { L1BlockId } from '@aztec/ethereum/l1-types'; -import { CheckpointNumber } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Fr } from '@aztec/foundation/curves/bn254'; import { toArray } from '@aztec/foundation/iterable'; import { createLogger } from '@aztec/foundation/log'; @@ -13,19 +12,10 @@ import { type CustomRange, mapRange, } from '@aztec/kv-store'; -import { type InboxBucket, InboxLeaf, updateInboxRollingHash } from '@aztec/stdlib/messaging'; +import { type InboxBucket, updateInboxRollingHash } from '@aztec/stdlib/messaging'; -import { - InboxBucketBoundaryNotSyncedError, - InboxBucketNotSyncedError, - L1ToL2MessagesNotReadyError, -} from '../errors.js'; -import { - type InboxMessage, - deserializeInboxMessage, - serializeInboxMessage, - updateRollingHash, -} from '../structs/inbox_message.js'; +import { InboxBucketBoundaryNotSyncedError, InboxBucketNotSyncedError } from '../errors.js'; +import { type InboxMessage, deserializeInboxMessage, serializeInboxMessage } from '../structs/inbox_message.js'; /** * Persisted snapshot of an Inbox rolling-hash bucket. Mirrors the fields the on-chain Inbox tracks per bucket, plus @@ -121,8 +111,6 @@ export class MessageStore { #lastSynchedL1Block: AztecAsyncSingleton; /** Stores total messages stored */ #totalMessageCount: AztecAsyncSingleton; - /** Stores the checkpoint number whose message tree is currently being filled on L1. */ - #inboxTreeInProgress: AztecAsyncSingleton; /** Stores the L1 finalized block as of the last successful message sync. */ #messagesFinalizedL1Block: AztecAsyncSingleton; /** Maps from Inbox bucket sequence number to its serialized snapshot. */ @@ -141,7 +129,6 @@ export class MessageStore { this.#l1ToL2MessageIndices = db.openMap('archiver_l1_to_l2_message_indices'); this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_id'); this.#totalMessageCount = db.openSingleton('archiver_l1_to_l2_message_count'); - this.#inboxTreeInProgress = db.openSingleton('archiver_inbox_tree_in_progress'); this.#messagesFinalizedL1Block = db.openSingleton('archiver_messages_finalized_l1_block'); this.#inboxBuckets = db.openMap('archiver_inbox_buckets'); this.#bucketTimestampToSeq = db.openMultiMap('archiver_inbox_bucket_timestamps'); @@ -218,7 +205,7 @@ export class MessageStore { // Check messages are inserted in increasing order, but allow reinserting messages. if (lastMessage && message.index <= lastMessage.index) { const existing = await this.#l1ToL2Messages.getAsync(this.indexToKey(message.index)); - if (existing && deserializeInboxMessage(existing).rollingHash.equals(message.rollingHash)) { + if (existing && deserializeInboxMessage(existing).inboxRollingHash.equals(message.inboxRollingHash)) { // We reinsert instead of skipping in case the message was re-orged and got added in a different L1 block. this.#log.trace(`Reinserting message with index ${message.index} in the store`); await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message)); @@ -231,20 +218,19 @@ export class MessageStore { ); } - // Check rolling hash is valid. - const previousRollingHash = lastMessage?.rollingHash ?? Buffer16.ZERO; - const expectedRollingHash = updateRollingHash(previousRollingHash, message.leaf); - if (!expectedRollingHash.equals(message.rollingHash)) { + // Check the compact-indexed messages arrive contiguously: the global insertion index of + // each message is exactly one past the previous one. + const expectedIndex = lastMessage === undefined ? 0n : lastMessage.index + 1n; + if (message.index !== expectedIndex) { throw new MessageStoreError( - `Invalid rolling hash for incoming L1 to L2 message ${message.leaf.toString()} ` + - `with index ${message.index} ` + - `(expected ${expectedRollingHash.toString()} from previous hash ${previousRollingHash} but got ${message.rollingHash.toString()})`, + `Invalid index ${message.index} for incoming L1 to L2 message ${message.leaf.toString()} ` + + `(expected ${expectedIndex})`, message, ); } - // Check the full-width consensus rolling hash is valid (AZIP-22 Fast Inbox). Runs alongside the legacy - // 128-bit check above until the streaming inbox flips on and the legacy hash is removed. + // Check the consensus rolling-hash chain is valid: each message's rolling hash must + // continue the chain from the previously inserted message. const previousInboxRollingHash = lastMessage?.inboxRollingHash ?? Fr.ZERO; const expectedInboxRollingHash = updateInboxRollingHash(previousInboxRollingHash, message.leaf); if (!expectedInboxRollingHash.equals(message.inboxRollingHash)) { @@ -257,18 +243,6 @@ export class MessageStore { ); } - // Check the compact-indexed messages arrive contiguously (AZIP-22 Fast Inbox): the global insertion index of - // each message is exactly one past the previous one, independent of the checkpoint it landed in. The flipped - // Inbox emits this compact totalMessagesInserted index, so the legacy per-checkpoint range no longer applies. - const expectedIndex = lastMessage === undefined ? 0n : lastMessage.index + 1n; - if (message.index !== expectedIndex) { - throw new MessageStoreError( - `Invalid index ${message.index} for incoming L1 to L2 message ${message.leaf.toString()} ` + - `(expected ${expectedIndex})`, - message, - ); - } - // Perform the insertions. await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message)); await this.#l1ToL2MessageIndices.set(this.leafToIndexKey(message.leaf), message.index); @@ -356,63 +330,19 @@ export class MessageStore { return msg ? deserializeInboxMessage(msg) : undefined; } - /** Returns the inbox tree-in-progress checkpoint number from L1, or undefined if not yet set. */ - public getInboxTreeInProgress(): Promise { - return this.#inboxTreeInProgress.getAsync(); - } - /** - * Atomically updates the message sync state: the L1 sync point, the inbox tree-in-progress marker, and - * (optionally) the L1 finalized block as of this sync. The finalized block is advanced monotonically. + * Atomically updates the message sync state: the L1 sync point and (optionally) the L1 finalized block as of this + * sync. The finalized block is advanced monotonically. */ - public setMessageSyncState( - l1Block: L1BlockId, - treeInProgress: bigint | undefined, - finalizedL1Block?: L1BlockId, - ): Promise { + public setMessageSyncState(l1Block: L1BlockId, finalizedL1Block?: L1BlockId): Promise { return this.db.transactionAsync(async () => { await this.setSynchedL1Block(l1Block); - if (treeInProgress !== undefined) { - await this.#inboxTreeInProgress.set(treeInProgress); - } else { - await this.#inboxTreeInProgress.delete(); - } if (finalizedL1Block !== undefined) { await this.maybeAdvanceFinalizedL1Block(finalizedL1Block); } }); } - public async getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise { - const treeInProgress = await this.#inboxTreeInProgress.getAsync(); - if (treeInProgress !== undefined && BigInt(checkpointNumber) >= treeInProgress) { - throw new L1ToL2MessagesNotReadyError(checkpointNumber, treeInProgress); - } - - const messages: Fr[] = []; - - const [startIndex, endIndex] = InboxLeaf.indexRangeForCheckpoint(checkpointNumber); - let lastIndex = startIndex - 1n; - - for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync({ - start: this.indexToKey(startIndex), - end: this.indexToKey(endIndex), - })) { - const msg = deserializeInboxMessage(msgBuffer); - if (msg.checkpointNumber !== checkpointNumber) { - throw new Error( - `L1 to L2 message with index ${msg.index} has invalid checkpoint number ${msg.checkpointNumber}`, - ); - } else if (msg.index !== lastIndex + 1n) { - throw new Error(`Expected L1 to L2 message with index ${lastIndex + 1n} but got ${msg.index}`); - } - lastIndex = msg.index; - messages.push(msg.leaf); - } - - return messages; - } - public async *iterateL1ToL2Messages(range: CustomRange = {}): AsyncIterableIterator { const entriesRange = mapRange(range, this.indexToKey); for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync(entriesRange)) { @@ -649,10 +579,26 @@ export class MessageStore { return Number(timestamp); } - public rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber: CheckpointNumber): Promise { - this.#log.debug(`Deleting L1 to L2 messages up to target checkpoint ${targetCheckpointNumber}`); - const startIndex = InboxLeaf.smallestIndexForCheckpoint(CheckpointNumber(targetCheckpointNumber + 1)); - return this.removeL1ToL2Messages(startIndex); + /** + * Removes every L1 to L2 message inserted after the given L1 block, so the message store matches the L1 Inbox state + * as of that block. Used when rolling the archiver back to an earlier checkpoint, whose L1 block is passed here. + * + * A bucket lives entirely within one L1 block, so the cut always falls on a bucket boundary and can be found from + * the bucket snapshots alone, without reading the messages being removed. + */ + public async rollbackL1ToL2MessagesAfterL1Block(l1BlockNumber: bigint): Promise { + this.#log.debug(`Deleting L1 to L2 messages inserted after L1 block ${l1BlockNumber}`); + let removeFromIndex: bigint | undefined; + for await (const snapBuffer of this.#inboxBuckets.valuesAsync({ reverse: true })) { + const snapshot = deserializeBucketSnapshot(snapBuffer); + if (snapshot.l1BlockNumber <= l1BlockNumber) { + break; + } + removeFromIndex = snapshot.firstMessageIndex; + } + if (removeFromIndex !== undefined) { + await this.removeL1ToL2Messages(removeFromIndex); + } } private indexToKey(index: bigint): number { diff --git a/yarn-project/archiver/src/structs/inbox_message.ts b/yarn-project/archiver/src/structs/inbox_message.ts index 137ee14b1044..a27192e6cd32 100644 --- a/yarn-project/archiver/src/structs/inbox_message.ts +++ b/yarn-project/archiver/src/structs/inbox_message.ts @@ -1,17 +1,12 @@ -import { CheckpointNumber } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; -import { keccak256 } from '@aztec/foundation/crypto/keccak'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { BufferReader, bigintToUInt64BE, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize'; +import { BufferReader, bigintToUInt64BE, serializeToBuffer } from '@aztec/foundation/serialize'; export type InboxMessage = { index: bigint; leaf: Fr; - checkpointNumber: CheckpointNumber; l1BlockNumber: bigint; l1BlockHash: Buffer32; - /** Legacy 128-bit keccak rolling hash of all messages inserted up to and including this one. */ - rollingHash: Buffer16; /** Consensus rolling hash (truncated sha256 chain) of all messages up to and including this one. */ inboxRollingHash: Fr; /** Sequence number of the Inbox bucket this message was absorbed into. */ @@ -20,19 +15,12 @@ export type InboxMessage = { bucketTimestamp: bigint; }; -export function updateRollingHash(currentRollingHash: Buffer16, leaf: Fr): Buffer16 { - const input = Buffer.concat([currentRollingHash.toBuffer(), leaf.toBuffer()]); - return Buffer16.fromBuffer(keccak256(input)); -} - export function serializeInboxMessage(message: InboxMessage): Buffer { return serializeToBuffer([ bigintToUInt64BE(message.index), message.leaf, message.l1BlockHash, bigintToUInt64BE(message.l1BlockNumber), - numToUInt32BE(message.checkpointNumber), - message.rollingHash, message.inboxRollingHash, bigintToUInt64BE(message.bucketSeq), bigintToUInt64BE(message.bucketTimestamp), @@ -45,8 +33,6 @@ export function deserializeInboxMessage(buffer: Buffer): InboxMessage { const leaf = reader.readObject(Fr); const l1BlockHash = reader.readObject(Buffer32); const l1BlockNumber = reader.readUInt64(); - const checkpointNumber = CheckpointNumber(reader.readNumber()); - const rollingHash = reader.readObject(Buffer16); const inboxRollingHash = reader.readObject(Fr); const bucketSeq = reader.readUInt64(); const bucketTimestamp = reader.readUInt64(); @@ -55,8 +41,6 @@ export function deserializeInboxMessage(buffer: Buffer): InboxMessage { leaf, l1BlockHash, l1BlockNumber, - checkpointNumber, - rollingHash, inboxRollingHash, bucketSeq, bucketTimestamp, diff --git a/yarn-project/archiver/src/test/fake_l1_state.ts b/yarn-project/archiver/src/test/fake_l1_state.ts index 97d05a98999c..101248149525 100644 --- a/yarn-project/archiver/src/test/fake_l1_state.ts +++ b/yarn-project/archiver/src/test/fake_l1_state.ts @@ -1,11 +1,10 @@ import type { BlobClientInterface } from '@aztec/blob-client/client'; import { type Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib'; -import { INITIAL_CHECKPOINT_NUMBER } from '@aztec/constants'; import type { CheckpointProposedLog, InboxContract, MessageSentLog, RollupContract } from '@aztec/ethereum/contracts'; import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts'; import type { ViemPublicClient } from '@aztec/ethereum/types'; import { type BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -31,8 +30,6 @@ import { toHex, } from 'viem'; -import { updateRollingHash } from '../structs/inbox_message.js'; - /** Configuration for the fake L1 state. */ export type FakeL1StateConfig = { /** Genesis archive root. */ @@ -105,10 +102,8 @@ type CheckpointData = { /** Data stored for a message. */ type MessageData = { l1BlockNumber: bigint; - checkpointNumber: CheckpointNumber; index: bigint; leaf: Fr; - rollingHash: Buffer16; inboxRollingHash: Fr; bucketSeq: bigint; }; @@ -142,7 +137,6 @@ export class FakeL1State { private l1BlockNumber: bigint; private checkpoints: CheckpointData[] = []; private messages: MessageData[] = []; - private messagesRollingHash: Buffer16 = Buffer16.ZERO; // Consensus rolling-hash and bucket-ring state, mirroring the on-chain Inbox. private messagesConsensusRollingHash: Fr = Fr.ZERO; private currentBucketSeq: bigint = 0n; @@ -175,20 +169,17 @@ export class FakeL1State { * Note: For most use cases, use `addCheckpoint` which creates both checkpoint and messages. * Use this method only when you need to add messages without creating a checkpoint (e.g., for reorg tests). */ - addMessages(checkpointNumber: CheckpointNumber, l1BlockNumber: bigint, messageLeaves: Fr[]): void { + addMessages(_checkpointNumber: CheckpointNumber, l1BlockNumber: bigint, messageLeaves: Fr[]): void { const timestamp = this.getTimestampAtL1Block(l1BlockNumber); messageLeaves.forEach(leaf => { - // Compact global insertion index (AZIP-22 Fast Inbox): position in the Inbox's insertion order. + // Compact global insertion index: the position in the Inbox's insertion order. const index = BigInt(this.messages.length); - this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, leaf); const { bucketSeq, inboxRollingHash } = this.absorbIntoBucket(leaf, timestamp); this.messages.push({ l1BlockNumber, - checkpointNumber, index, leaf, - rollingHash: this.messagesRollingHash, inboxRollingHash, bucketSeq, }); @@ -213,20 +204,16 @@ export class FakeL1State { /** Rebuilds all per-message derived state (rolling hashes and bucket assignments) after the message set changes. */ private recomputeDerivedMessageState(): void { - this.messagesRollingHash = Buffer16.ZERO; this.messagesConsensusRollingHash = Fr.ZERO; this.currentBucketSeq = 0n; this.currentBucketTimestamp = 0n; this.currentBucketMsgCount = 0; this.messages.forEach((msg, i) => { - this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, msg.leaf); const { bucketSeq, inboxRollingHash } = this.absorbIntoBucket( msg.leaf, this.getTimestampAtL1Block(msg.l1BlockNumber), ); - // Keep the compact global insertion index contiguous after the message set changes (e.g. reorgs). msg.index = BigInt(i); - msg.rollingHash = this.messagesRollingHash; msg.inboxRollingHash = inboxRollingHash; msg.bucketSeq = bucketSeq; }); @@ -469,11 +456,6 @@ export class FakeL1State { return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.checkpoint; } - /** Gets messages for a checkpoint. */ - getMessages(checkpointNumber: CheckpointNumber): Fr[] { - return this.messages.filter(m => m.checkpointNumber === checkpointNumber).map(m => m.leaf); - } - /** Gets the blobs for a checkpoint. */ getCheckpointBlobs(checkpointNumber: CheckpointNumber): Blob[] { return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.blobs ?? []; @@ -519,33 +501,26 @@ export class FakeL1State { const mockInbox = mock(); mockInbox.getState.mockImplementation((opts: { blockTag?: string; blockNumber?: bigint } = {}) => { - // Filter messages visible at the given block number (or all if not specified) const blockNumber = opts.blockNumber ?? this.l1BlockNumber; const visibleMessages = this.messages.filter(m => m.l1BlockNumber <= blockNumber); + return Promise.resolve({ totalMessagesInserted: BigInt(visibleMessages.length) }); + }); - // treeInProgress must be > any sealed checkpoint. On L1, a checkpoint can only be proposed - // after its messages are sealed, so treeInProgress > checkpointNumber for all published checkpoints. - const maxFromMessages = - visibleMessages.length > 0 ? Math.max(...visibleMessages.map(m => Number(m.checkpointNumber))) + 1 : 0; - const maxFromCheckpoints = - this.checkpoints.length > 0 - ? Math.max( - ...this.checkpoints - .filter(cp => !cp.pruned && cp.l1BlockNumber <= blockNumber) - .map(cp => Number(cp.checkpointNumber)), - 0, - ) + 1 - : 0; - const treeInProgress = Math.max(maxFromMessages, maxFromCheckpoints, INITIAL_CHECKPOINT_NUMBER); - - // Compute rolling hash only for visible messages - const rollingHash = - visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1].rollingHash : Buffer16.ZERO; - + // Mirror the on-chain Inbox current bucket: its consensus rolling hash and cumulative total are the live chain + // position the archiver's message sync compares against. + mockInbox.getCurrentBucket.mockImplementation((opts: { blockTag?: string; blockNumber?: bigint } = {}) => { + const blockNumber = opts.blockNumber ?? this.l1BlockNumber; + const visibleMessages = this.messages.filter(m => m.l1BlockNumber <= blockNumber); + const last = visibleMessages.at(-1); + if (last === undefined) { + return Promise.resolve({ rollingHash: Fr.ZERO, totalMsgCount: 0n, timestamp: 0n, msgCount: 0 }); + } + const msgCount = visibleMessages.filter(m => m.bucketSeq === last.bucketSeq).length; return Promise.resolve({ - messagesRollingHash: rollingHash, - totalMessagesInserted: BigInt(visibleMessages.length), - treeInProgress: BigInt(treeInProgress), + rollingHash: last.inboxRollingHash, + totalMsgCount: BigInt(visibleMessages.length), + timestamp: this.getTimestampAtL1Block(last.l1BlockNumber), + msgCount, }); }); @@ -676,10 +651,8 @@ export class FakeL1State { l1TransactionHash: `0x${msg.l1BlockNumber.toString(16)}` as `0x${string}`, l1BlockTimestamp: this.getTimestampAtL1Block(msg.l1BlockNumber), args: { - checkpointNumber: msg.checkpointNumber, index: msg.index, leaf: msg.leaf, - rollingHash: msg.rollingHash, inboxRollingHash: msg.inboxRollingHash, bucketSeq: msg.bucketSeq, }, @@ -704,7 +677,6 @@ export class FakeL1State { args: { index: msg.index, leaf: msg.leaf, - rollingHash: msg.rollingHash, inboxRollingHash: msg.inboxRollingHash, bucketSeq: msg.bucketSeq, }, diff --git a/yarn-project/archiver/src/test/mock_archiver.ts b/yarn-project/archiver/src/test/mock_archiver.ts index 029ea7f8a205..357c09d673e0 100644 --- a/yarn-project/archiver/src/test/mock_archiver.ts +++ b/yarn-project/archiver/src/test/mock_archiver.ts @@ -1,4 +1,3 @@ -import type { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; @@ -13,18 +12,10 @@ import { MockL2BlockSource } from './mock_l2_block_source.js'; export class MockArchiver extends MockL2BlockSource implements L2BlockSource, L1ToL2MessageSource { private messageSource = new MockL1ToL2MessageSource(0); - public setL1ToL2Messages(checkpointNumber: CheckpointNumber, msgs: Fr[]) { - this.messageSource.setL1ToL2Messages(checkpointNumber, msgs); - } - public setInboxBucket(bucket: InboxBucket, msgs: Fr[] = []) { this.messageSource.setInboxBucket(bucket, msgs); } - getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise { - return this.messageSource.getL1ToL2Messages(checkpointNumber); - } - getL1ToL2MessageIndex(_l1ToL2Message: Fr): Promise { return this.messageSource.getL1ToL2MessageIndex(_l1ToL2Message); } @@ -63,12 +54,11 @@ export class MockPrefilledArchiver extends MockArchiver { } public setPrefilled(prefilled: { checkpoint: Checkpoint; messages: Fr[] }[]) { - for (const { checkpoint, messages } of prefilled) { + for (const { checkpoint } of prefilled) { this.prefilled[checkpoint.number - 1] = checkpoint; if (checkpoint.blocks.length !== 1) { throw new Error('Prefilled checkpoint must only have 1 block at the moment.'); } - this.setL1ToL2Messages(checkpoint.number, messages); } for (const { checkpoint, messages } of prefilled) { diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index 7a8940a12a23..53b652f8d279 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -7,16 +7,11 @@ import type { InboxBucket, L1ToL2MessageSource } from '@aztec/stdlib/messaging'; * A mocked implementation of L1ToL2MessageSource to be used in tests. */ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { - private messagesPerCheckpoint = new Map(); private buckets = new Map(); private messagesPerBucket = new Map(); constructor(private blockNumber: number) {} - public setL1ToL2Messages(checkpointNumber: CheckpointNumber, msgs: Fr[]) { - this.messagesPerCheckpoint.set(checkpointNumber, msgs); - } - public setInboxBucket(bucket: InboxBucket, msgs: Fr[] = []) { this.buckets.set(bucket.seq, bucket); this.messagesPerBucket.set(bucket.seq, msgs); @@ -26,10 +21,6 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { this.blockNumber = blockNumber; } - getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise { - return Promise.resolve(this.messagesPerCheckpoint.get(checkpointNumber) ?? []); - } - getL1ToL2MessageIndex(_l1ToL2Message: Fr): Promise { throw new Error('Method not implemented.'); } diff --git a/yarn-project/archiver/src/test/mock_structs.ts b/yarn-project/archiver/src/test/mock_structs.ts index 7cd23455029e..5e6539ce0c59 100644 --- a/yarn-project/archiver/src/test/mock_structs.ts +++ b/yarn-project/archiver/src/test/mock_structs.ts @@ -1,9 +1,9 @@ -import { MAX_L1_TO_L2_MSGS_PER_CHECKPOINT, MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; +import { MAX_L1_TO_L2_MSGS_PER_BLOCK, MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; import { makeTuple } from '@aztec/foundation/array'; import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { times, timesParallel } from '@aztec/foundation/collection'; -import { randomBigInt, randomInt } from '@aztec/foundation/crypto/random'; +import { randomBigInt } from '@aztec/foundation/crypto/random'; import type { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -18,20 +18,18 @@ import { makeCheckpointAttestationFromCheckpoint } from '@aztec/stdlib/testing'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { PartialStateReference, StateReference, TxEffect } from '@aztec/stdlib/tx'; -import { type InboxMessage, updateRollingHash } from '../structs/inbox_message.js'; +import type { InboxMessage } from '../structs/inbox_message.js'; export function makeInboxMessage( - previousRollingHash = Buffer16.ZERO, + previousInboxRollingHash = Fr.ZERO, overrides: Partial = {}, ): InboxMessage { - const { checkpointNumber = CheckpointNumber(randomInt(100) + 1) } = overrides; const { l1BlockNumber = randomBigInt(100n) + 1n } = overrides; const { l1BlockHash = Buffer32.random() } = overrides; const { leaf = Fr.random() } = overrides; - const { rollingHash = updateRollingHash(previousRollingHash, leaf) } = overrides; // Compact global insertion index: defaults to the first slot. const { index = 0n } = overrides; - const { inboxRollingHash = updateInboxRollingHash(Fr.ZERO, leaf) } = overrides; + const { inboxRollingHash = updateInboxRollingHash(previousInboxRollingHash, leaf) } = overrides; // Default each message to its own bucket, keyed monotonically off its global index. const { bucketSeq = index + 1n } = overrides; const { bucketTimestamp = index + 1n } = overrides; @@ -39,75 +37,55 @@ export function makeInboxMessage( return { index, leaf, - checkpointNumber, l1BlockNumber, l1BlockHash, - rollingHash, inboxRollingHash, bucketSeq, bucketTimestamp, }; } +/** + * Builds a contiguous run of `totalCount` inbox messages with compact global indices starting at `initialIndex` + * and a chained consensus rolling hash starting from `initialInboxHash`. + */ export function makeInboxMessages( totalCount: number, opts: { - initialHash?: Buffer16; initialInboxHash?: Fr; initialIndex?: bigint; - initialCheckpointNumber?: CheckpointNumber; - messagesPerCheckpoint?: number; overrideFn?: (msg: InboxMessage, index: number) => InboxMessage; } = {}, ): InboxMessage[] { - const { - initialHash = Buffer16.ZERO, - initialInboxHash = Fr.ZERO, - initialIndex = 0n, - overrideFn = msg => msg, - initialCheckpointNumber = CheckpointNumber(1), - messagesPerCheckpoint = 1, - } = opts; + const { initialInboxHash = Fr.ZERO, initialIndex = 0n, overrideFn = msg => msg } = opts; const messages: InboxMessage[] = []; - let rollingHash = initialHash; let inboxRollingHash = initialInboxHash; for (let i = 0; i < totalCount; i++) { - const checkpointNumber = CheckpointNumber.fromBigInt( - BigInt(initialCheckpointNumber) + BigInt(i) / BigInt(messagesPerCheckpoint), - ); const leaf = Fr.random(); - // Compact global insertion index (AZIP-22 Fast Inbox): contiguous from initialIndex, independent of checkpoint. + inboxRollingHash = updateInboxRollingHash(inboxRollingHash, leaf); const message = overrideFn( - makeInboxMessage(rollingHash, { + makeInboxMessage(Fr.ZERO, { leaf, - checkpointNumber, index: initialIndex + BigInt(i), - inboxRollingHash: updateInboxRollingHash(inboxRollingHash, leaf), + inboxRollingHash, }), i, ); - rollingHash = message.rollingHash; - inboxRollingHash = message.inboxRollingHash; messages.push(message); } return messages; } -/** Creates inbox messages distributed across multiple blocks with proper checkpoint numbering. */ -export function makeInboxMessagesWithFullBlocks( - blockCount: number, - opts: { initialCheckpointNumber?: CheckpointNumber } = {}, -): InboxMessage[] { - const { initialCheckpointNumber = CheckpointNumber(13) } = opts; - return makeInboxMessages(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT * blockCount, { - // Keep the compact global index from makeInboxMessages; only spread the (now-vestigial) checkpoint assignment - // across blocks so multi-block coverage still exercises differing checkpoint numbers. +/** + * Creates `blockCount` full buckets of `MAX_L1_TO_L2_MSGS_PER_BLOCK` inbox messages each, with compact indices and one + * bucket sequence per block. + */ +export function makeInboxMessagesWithFullBlocks(blockCount: number): InboxMessage[] { + return makeInboxMessages(MAX_L1_TO_L2_MSGS_PER_BLOCK * blockCount, { overrideFn: (msg, i) => { - const checkpointNumber = CheckpointNumber( - initialCheckpointNumber + Math.floor(i / MAX_L1_TO_L2_MSGS_PER_CHECKPOINT), - ); - return { ...msg, checkpointNumber }; + const bucketSeq = BigInt(Math.floor(i / MAX_L1_TO_L2_MSGS_PER_BLOCK)) + 1n; + return { ...msg, bucketSeq, bucketTimestamp: bucketSeq }; }, }); } diff --git a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts index 3b42be8a113a..3fc1008c01ee 100644 --- a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts @@ -1,4 +1,3 @@ -import { L1ToL2MessagesNotReadyError } from '@aztec/archiver'; import type { EpochCacheInterface } from '@aztec/epoch-cache'; import { type FeeHeader, RollupContract } from '@aztec/ethereum/contracts'; import { @@ -24,9 +23,10 @@ import { } from '@aztec/stdlib/block'; import type { ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { ContractDataSource } from '@aztec/stdlib/contract'; +import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; import type { MerkleTreeWriteOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; -import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; +import type { InboxBucket, L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { mockTx } from '@aztec/stdlib/testing'; import { AppendOnlyTreeSnapshot, MerkleTreeId } from '@aztec/stdlib/trees'; @@ -114,6 +114,26 @@ describe('NodePublicCallsSimulator', () => { gasFees: GasFees.empty(), }); + /** + * Mocks the Inbox so the next-block prediction selects a two-message bundle: the fork's message total (0) + * resolves to bucket 0, and bucket 1 is lag-eligible and holds both messages. + */ + const mockInboxSelection = () => { + const makeBucket = (seq: bigint, totalMsgCount: bigint): InboxBucket => ({ + seq, + inboxRollingHash: Fr.ZERO, + totalMsgCount, + timestamp: 0n, + msgCount: Number(totalMsgCount), + lastMessageIndex: totalMsgCount === 0n ? 0n : totalMsgCount - 1n, + }); + const bundle = [new Fr(0x1234), new Fr(0x5678)]; + l1ToL2MessageSource.getInboxBucketByTotalMsgCount.mockResolvedValue(makeBucket(0n, 0n)); + l1ToL2MessageSource.getLatestInboxBucketAtOrBefore.mockResolvedValue(makeBucket(1n, 2n)); + l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets.mockResolvedValue(bundle); + return bundle; + }; + const lowGasTx = () => mockTx(0x10000, { numberOfNonRevertiblePublicCallRequests: 0, @@ -140,9 +160,18 @@ describe('NodePublicCallsSimulator', () => { (merkleTreeFork as unknown as { [Symbol.asyncDispose]: () => Promise })[Symbol.asyncDispose] = () => Promise.resolve(); worldStateSynchronizer.fork.mockResolvedValue(merkleTreeFork); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); + merkleTreeFork.getTreeInfo.mockResolvedValue({ + treeId: MerkleTreeId.L1_TO_L2_MESSAGE_TREE, + root: Buffer.alloc(32), + size: 0n, + depth: 16, + }); blockSource.getPendingChainValidationStatus.mockResolvedValue({ valid: true }); blockSource.getProposedCheckpointData.mockResolvedValue(undefined); + // No Inbox bucket resolves to the fork's message total by default, so the next-block message prediction + // bails out and tests see the bare tip state unless they opt into it. + l1ToL2MessageSource.getInboxBucketByTotalMsgCount.mockResolvedValue(undefined); + epochCache.getL1Constants.mockReturnValue(EmptyL1RollupConstants); globalVariableBuilder.buildCheckpointGlobalVariables.mockImplementation((_c, _f, slotNumber) => Promise.resolve(checkpointGlobals(slotNumber)), @@ -222,17 +251,31 @@ describe('NodePublicCallsSimulator', () => { expect(rollupContract.getManaTarget).not.toHaveBeenCalled(); }); - it('does not insert L1-to-L2 messages', async () => { + it('appends the message bundle the next block would consume', async () => { const tx = await lowGasTx(); setupMidCheckpoint(); blockSource.getBlockData.mockImplementation((query: BlockQuery) => Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(42)) : undefined), ); mockNextL1Slot(SlotNumber(100)); + const bundle = mockInboxSelection(); await simulator.simulate(tx); - expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled(); + expect(merkleTreeFork.appendLeaves).toHaveBeenCalledWith(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, bundle); + }); + + it('simulates against the tip when the parent Inbox bucket is not synced', async () => { + const tx = await lowGasTx(); + setupMidCheckpoint(); + blockSource.getBlockData.mockImplementation((query: BlockQuery) => + Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(42)) : undefined), + ); + mockNextL1Slot(SlotNumber(100)); + // Default mock: no bucket resolves the fork's message total. + + await expect(simulator.simulate(tx)).resolves.toBeDefined(); + expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled(); }); @@ -245,8 +288,7 @@ describe('NodePublicCallsSimulator', () => { await expect(simulator.simulate(tx)).rejects.toThrow(); - // Must not treat the next block as opening a new checkpoint and re-insert the ongoing checkpoint's messages. - expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled(); + // Must not treat the next block as opening a new checkpoint. expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled(); expect(globalVariableBuilder.buildCheckpointGlobalVariables).not.toHaveBeenCalled(); }); @@ -281,36 +323,19 @@ describe('NodePublicCallsSimulator', () => { expect(plan?.chainTipsOverride).toEqual({ pending: CheckpointNumber(1), proven: CheckpointNumber(1) }); }); - it('inserts L1-to-L2 messages for the next checkpoint', async () => { - const tx = await lowGasTx(); - const messages = [Fr.fromString('0x1234'), Fr.fromString('0x5678')]; - blockSource.getL2Tips.mockResolvedValue(setupBoundary()); - blockSource.getBlockData.mockImplementation((query: BlockQuery) => - Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(5)) : undefined), - ); - mockNextL1Slot(SlotNumber(20)); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(messages); - - await simulator.simulate(tx); - - // targetCheckpoint = proposedCheckpoint.number + 1 - expect(l1ToL2MessageSource.getL1ToL2Messages).toHaveBeenCalledWith(CheckpointNumber(2)); - const [treeId, appended] = merkleTreeFork.appendLeaves.mock.calls[0]; - expect(treeId).toEqual(MerkleTreeId.L1_TO_L2_MESSAGE_TREE); - expect(appended.slice(0, 2)).toEqual(messages); - }); - - it('tolerates L1ToL2MessagesNotReadyError and simulates without messages', async () => { + it('appends the message bundle the next block would consume', async () => { const tx = await lowGasTx(); blockSource.getL2Tips.mockResolvedValue(setupBoundary()); blockSource.getBlockData.mockImplementation((query: BlockQuery) => Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(5)) : undefined), ); mockNextL1Slot(SlotNumber(20)); - l1ToL2MessageSource.getL1ToL2Messages.mockRejectedValue(new L1ToL2MessagesNotReadyError(CheckpointNumber(2), 0n)); + const bundle = mockInboxSelection(); await expect(simulator.simulate(tx)).resolves.toBeDefined(); - expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled(); + + // Only the first block's worth of messages: a fresh checkpoint starts its per-checkpoint budget at the tip. + expect(merkleTreeFork.appendLeaves).toHaveBeenCalledWith(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, bundle); }); it('targets parentSlot + 1 and carries the parent overrides when pipelining on a proposed checkpoint', async () => { diff --git a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.ts b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.ts index a563ac5432c1..07c9ed0adde9 100644 --- a/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.ts +++ b/yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.ts @@ -1,4 +1,4 @@ -import { L1ToL2MessagesNotReadyError } from '@aztec/archiver'; +import { INBOX_LAG_SECONDS, MAX_L1_TO_L2_MSGS_PER_BLOCK, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants'; import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache'; import type { EpochCacheInterface } from '@aztec/epoch-cache'; import { @@ -8,21 +8,21 @@ import { } from '@aztec/ethereum/contracts'; import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { compactArray } from '@aztec/foundation/collection'; -import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { BadRequestError } from '@aztec/foundation/json-rpc'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; -import { isErrorClass } from '@aztec/foundation/types'; +import { type InboxBucketSource, selectInboxBucketForBlock } from '@aztec/sequencer-client'; import { type AvmSimulator, PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2BlockSource, L2Tips } from '@aztec/stdlib/block'; import { type ProposedCheckpointData, buildCheckpointSimulationOverridesPlan } from '@aztec/stdlib/checkpoint'; import type { ContractDataSource } from '@aztec/stdlib/contract'; -import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; -import { type L1ToL2MessageSource, appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging'; +import type { MerkleTreeWriteOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; +import { type L1ToL2MessageSource, appendL1ToL2MessagesToTree, getInboxCutoffTimestamp } from '@aztec/stdlib/messaging'; import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p'; +import { MerkleTreeId } from '@aztec/stdlib/trees'; import { type GlobalVariableBuilder, GlobalVariables, @@ -34,6 +34,9 @@ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-clien import { applyPublicDataOverrides } from './public_data_overrides.js'; +/** Inbox queries the simulator needs to predict the message bundle the next block would consume. */ +type SimulatorInboxSource = InboxBucketSource & Pick; + /** Config fields the simulator needs — a narrow subset of `AztecNodeConfig`. */ export interface NodePublicCallsSimulatorConfig { /** Maximum total gas limit accepted for an incoming simulation. */ @@ -46,7 +49,8 @@ export interface NodePublicCallsSimulatorConfig { export interface NodePublicCallsSimulatorDeps { blockSource: L2BlockSource; worldStateSynchronizer: WorldStateSynchronizer; - l1ToL2MessageSource: L1ToL2MessageSource; + /** Inbox bucket queries, used to predict the L1-to-L2 messages the next block will consume. */ + l1ToL2MessageSource: SimulatorInboxSource; contractDataSource: ContractDataSource; globalVariableBuilder: GlobalVariableBuilder; /** @@ -78,16 +82,20 @@ export interface NodePublicCallsSimulatorDeps { * - **When the next block continues an in-progress checkpoint** (the latest proposed block is ahead of * the proposed-checkpoint frontier): every block in a checkpoint shares the same * `CheckpointGlobalVariables`, so we copy the latest proposed block's globals verbatim and only - * bump the block number. No L1 calls, no L1-to-L2 message insertion. + * bump the block number. No L1 calls. * - **When the next block opens a new checkpoint** (the latest proposed block coincides with the * proposed-checkpoint frontier): we compute fresh globals for the slot the next block will land in, * applying the same `SimulationOverridesPlan` the sequencer applies so the simulated mana min fee * matches what the sequencer will write into the block header. + * + * Either way it also predicts the L1-to-L2 message bundle the next block would consume and appends it + * to the fork, so a transaction consuming a message that is in the Inbox but not yet in a block + * simulates against the state it will actually run in. */ export class NodePublicCallsSimulator { private readonly blockSource: L2BlockSource; private readonly worldStateSynchronizer: WorldStateSynchronizer; - private readonly l1ToL2MessageSource: L1ToL2MessageSource; + private readonly l1ToL2MessageSource: SimulatorInboxSource; private readonly contractDataSource: ContractDataSource; private readonly globalVariableBuilder: GlobalVariableBuilder; private readonly rollupContract: RollupContract | undefined; @@ -97,6 +105,7 @@ export class NodePublicCallsSimulator { private readonly avmSimulator?: AvmSimulator; private readonly telemetry: TelemetryClient; private readonly log: Logger; + private readonly dateProvider = new DateProvider(); constructor(deps: NodePublicCallsSimulatorDeps) { this.blockSource = deps.blockSource; @@ -157,9 +166,7 @@ export class NodePublicCallsSimulator { // the proposed-checkpoint terminating block; it opens a new checkpoint when they coincide. const atCheckpointBoundary = proposedCheckpointLastBlock === l2Tips.proposed.number; - // `targetCheckpoint` is the checkpoint whose L1-to-L2 messages must be inserted into the fork - // before simulation. Only set when opening a new checkpoint, where the next block is its first block. - const { globalVariables: newGlobalVariables, targetCheckpoint } = atCheckpointBoundary + const { globalVariables: newGlobalVariables } = atCheckpointBoundary ? await this.buildGlobalVariablesForNewCheckpoint(l2Tips, proposedCheckpointData, blockNumber) : { globalVariables: await this.copyGlobalVariablesFromLatestProposedBlock(latestBlockNumber, blockNumber) }; @@ -169,7 +176,7 @@ export class NodePublicCallsSimulator { const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, this.avmSimulator, - new DateProvider(), + this.dateProvider, this.telemetry, this.log.getBindings(), ); @@ -184,18 +191,14 @@ export class NodePublicCallsSimulator { // Ensure world-state has caught up with the latest block we loaded from the archiver await this.worldStateSynchronizer.syncImmediate(latestBlockNumber); - const nextCheckpointMessages = await this.getNextCheckpointMessages(targetCheckpoint); - - // Request a new fork of the world state at the latest block number, and apply any overrides and next checkpoint messages to it before simulation + // Request a new fork of the world state at the latest block number, then apply the next block's predicted + // L1-to-L2 message bundle and any caller overrides to it before simulation. await using merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber); - if (nextCheckpointMessages !== undefined) { - this.log.debug( - `Appending ${nextCheckpointMessages.length} L1-to-L2 messages to the world state tree for the next checkpoint`, - { checkpointNumber: targetCheckpoint }, - ); - await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages); - } + await this.appendPredictedL1ToL2Messages(merkleTreeFork, { + slotNumber: newGlobalVariables.slotNumber, + checkpointStartBlock: atCheckpointBoundary ? undefined : proposedCheckpointLastBlock, + }); await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage); @@ -236,47 +239,88 @@ export class NodePublicCallsSimulator { } /** - * Fetches the next checkpoint's L1-to-L2 messages to insert into the fork before simulation. Only set - * when opening a new checkpoint; when continuing an in-progress checkpoint the ongoing checkpoint's - * messages were already applied when its first block synced, so inserting here would double-count them - * — which is why a missing header for the latest proposed block throws rather than falling through to - * this path. A not-ready or failed fetch degrades to simulating without the messages rather than - * failing the request. + * Appends the L1-to-L2 message bundle the next block would consume to the simulation fork, so a transaction + * consuming a message that has reached the Inbox but no block yet simulates against the state it will run in. + * Runs the same bucket selection the sequencer runs (lag eligibility plus the per-block and per-checkpoint caps), + * treating the next block as non-final: the censorship cutoff only widens consumption on a checkpoint's last + * block, and the node cannot know whether the next block is it. + * + * Best-effort. Any failure — Inbox buckets not synced yet, a torn archiver snapshot — leaves the fork at the tip + * state, which is what the transaction sees if the next block consumes nothing. */ - private async getNextCheckpointMessages(targetCheckpoint: CheckpointNumber | undefined): Promise { - if (targetCheckpoint === undefined) { - return undefined; - } + private async appendPredictedL1ToL2Messages( + fork: MerkleTreeWriteOperations, + opts: { + /** Slot the next block lands in; anchors the censorship cutoff. */ + slotNumber: SlotNumber; + /** Last block of the checkpoint the next block extends; undefined when the next block opens a checkpoint. */ + checkpointStartBlock: BlockNumber | undefined; + }, + ): Promise { try { - return await this.l1ToL2MessageSource.getL1ToL2Messages(targetCheckpoint); - } catch (err) { - if (isErrorClass(err, L1ToL2MessagesNotReadyError)) { - this.log.warn( - `L1-to-L2 messages for checkpoint ${targetCheckpoint} are not ready yet (simulating without them)`, - { checkpointNumber: targetCheckpoint }, - ); - } else { - this.log.error( - `Failed to get L1-to-L2 messages for checkpoint ${targetCheckpoint} (simulating without them)`, - err, - { checkpointNumber: targetCheckpoint }, - ); + const parentTotalMsgCount = (await fork.getTreeInfo(MerkleTreeId.L1_TO_L2_MESSAGE_TREE)).size; + const parentBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(parentTotalMsgCount); + if (parentBucket === undefined) { + this.log.debug(`Inbox bucket at message total ${parentTotalMsgCount} not synced; simulating against the tip`, { + parentTotalMsgCount, + }); + return; } - return undefined; + + // Origin of the per-checkpoint cap: the total consumed as of the checkpoint's parent. A block extending an + // in-progress checkpoint reads it off that checkpoint's parent block; a block opening one starts from the tip. + const checkpointStartTotalMsgCount = + opts.checkpointStartBlock === undefined + ? parentTotalMsgCount + : await this.getConsumedMessageTotal(opts.checkpointStartBlock); + if (checkpointStartTotalMsgCount === undefined) { + this.log.debug(`Block ${opts.checkpointStartBlock} has no header on this node; simulating against the tip`); + return; + } + + const selection = await selectInboxBucketForBlock({ + messageSource: this.l1ToL2MessageSource, + now: BigInt(Math.floor(this.dateProvider.now() / 1000)), + lagSeconds: BigInt(INBOX_LAG_SECONDS), + parent: { seq: parentBucket.seq, totalMsgCount: parentBucket.totalMsgCount }, + checkpointStartTotalMsgCount, + perBlockCap: MAX_L1_TO_L2_MSGS_PER_BLOCK, + perCheckpointCap: MAX_L1_TO_L2_MSGS_PER_CHECKPOINT, + isLastBlock: false, + cutoffTimestamp: getInboxCutoffTimestamp(opts.slotNumber, this.epochCache.getL1Constants(), INBOX_LAG_SECONDS), + }); + if (!selection.consume || selection.bundle.length === 0) { + return; + } + + await appendL1ToL2MessagesToTree(fork, selection.bundle); + this.log.debug(`Appended ${selection.bundle.length} predicted L1-to-L2 messages to the simulation fork`, { + bucketSeq: selection.bucket.seq, + messageCount: selection.bundle.length, + }); + } catch (err) { + this.log.verbose(`Could not predict the next block's L1-to-L2 messages, simulating against the tip: ${err}`); + } + } + + /** Cumulative Inbox message total consumed as of `blockNumber`, i.e. its L1-to-L2 message tree leaf count. */ + private async getConsumedMessageTotal(blockNumber: BlockNumber): Promise { + if (blockNumber === BlockNumber.ZERO) { + return 0n; } + const block = await this.blockSource.getBlockData({ number: blockNumber }); + return block === undefined ? undefined : BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex); } /** * Continues an in-progress checkpoint: the next block extends the checkpoint the latest proposed * block belongs to. Every block in a checkpoint shares the same `CheckpointGlobalVariables`, so the * next block's globals are the latest proposed block's globals with only the block number bumped — - * including the proposer's real coinbase/feeRecipient. No L1 reads and no L1-to-L2 message insertion - * happen here. + * including the proposer's real coinbase/feeRecipient. No L1 reads happen here. * * A missing header means the archiver reported a proposed tip via `getL2Tips` but no longer has its * data (a torn snapshot). We throw a transient/retryable error rather than treating the next block as - * opening a new checkpoint: the fork at `latestBlockNumber` already contains the ongoing checkpoint's - * L1-to-L2 messages, so inserting the next checkpoint's messages would append them a second time. + * opening a new checkpoint, whose globals would be built for the wrong slot. */ private async copyGlobalVariablesFromLatestProposedBlock( latestBlockNumber: BlockNumber, @@ -298,19 +342,14 @@ export class NodePublicCallsSimulator { * sequencer applies so the simulated mana min fee matches what the sequencer will write into the * block header. Coinbase and fee recipient stay zero (we cannot know the future proposer's payout * addresses), unlike continuing an in-progress checkpoint which inherits the real ones from the - * proposed header. Returns the target checkpoint so the caller inserts that checkpoint's L1-to-L2 - * messages into the fork. + * proposed header. */ private async buildGlobalVariablesForNewCheckpoint( l2Tips: L2Tips, proposedCheckpointData: ProposedCheckpointData | undefined, blockNumber: BlockNumber, - ): Promise<{ globalVariables: GlobalVariables; targetCheckpoint: CheckpointNumber }> { + ): Promise<{ globalVariables: GlobalVariables }> { const checkpointedCheckpointNumber = l2Tips.checkpointed.checkpoint.number; - // The new checkpoint sits on top of the proposed one when pipelining, otherwise on the - // checkpointed tip. The target slot and the overrides plan both derive from the single - // `proposedCheckpointData` read, so they cannot disagree about the proposed parent. - const proposedCheckpointNumber = proposedCheckpointData?.checkpointNumber ?? checkpointedCheckpointNumber; const targetSlot = this.computeTargetSlot(proposedCheckpointData); const plan = await this.buildSimulationOverridesPlan(proposedCheckpointData, checkpointedCheckpointNumber); @@ -324,7 +363,6 @@ export class NodePublicCallsSimulator { return { globalVariables: GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }), - targetCheckpoint: CheckpointNumber(proposedCheckpointNumber + 1), }; } 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 3df0f39ed9a6..3b4d4300f13f 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1568,27 +1568,19 @@ describe('aztec node', () => { }); }); - describe('getL1ToL2MessageCheckpoint', () => { - it('returns the checkpoint of the block that consumed the message', async () => { + describe('getL1ToL2MessageIndex', () => { + it('returns the compact leaf index the node assigned to the message', async () => { const msg = Fr.random(); - l1ToL2MessageSource.getL1ToL2MessageIndex.mockResolvedValue(0n); - // The first block (checkpoint 1) consumed message index 0: its L1-to-L2 tree leaf count is 1 > 0. - l2BlockSource.getBlockNumber.mockResolvedValue(BlockNumber(1)); - l2BlockSource.getBlockData.mockResolvedValue({ - checkpointNumber: CheckpointNumber(1), - header: { state: { l1ToL2MessageTree: { nextAvailableLeafIndex: 1 } } }, - } as unknown as BlockData); + l1ToL2MessageSource.getL1ToL2MessageIndex.mockResolvedValue(7n); - const result = await node.getL1ToL2MessageCheckpoint(msg); - expect(result).toEqual(CheckpointNumber(1)); + expect(await node.getL1ToL2MessageIndex(msg)).toEqual(7n); }); it('returns undefined when the message is not found', async () => { const msg = Fr.random(); l1ToL2MessageSource.getL1ToL2MessageIndex.mockResolvedValue(undefined); - const result = await node.getL1ToL2MessageCheckpoint(msg); - expect(result).toBeUndefined(); + expect(await node.getL1ToL2MessageIndex(msg)).toBeUndefined(); }); }); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index b3a3ee8deb64..b7768e5416f9 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -674,10 +674,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return this.worldStateQueries.getL1ToL2MessageMembershipWitness(referenceBlock, l1ToL2Message); } - public getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { - return this.worldStateQueries.getL1ToL2MessageCheckpoint(l1ToL2Message); - } - public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { return this.worldStateQueries.getL1ToL2MessageIndex(l1ToL2Message); } diff --git a/yarn-project/aztec-node/src/modules/node_world_state_queries.ts b/yarn-project/aztec-node/src/modules/node_world_state_queries.ts index d7ed5e9ae89f..0bbdefb67302 100644 --- a/yarn-project/aztec-node/src/modules/node_world_state_queries.ts +++ b/yarn-project/aztec-node/src/modules/node_world_state_queries.ts @@ -1,10 +1,5 @@ -import { - ARCHIVE_HEIGHT, - INITIAL_L2_BLOCK_NUM, - type L1_TO_L2_MSG_TREE_HEIGHT, - type NOTE_HASH_TREE_HEIGHT, -} from '@aztec/constants'; -import { BlockNumber, type CheckpointNumber, type EpochNumber } from '@aztec/foundation/branded-types'; +import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; +import { BlockNumber, type EpochNumber } from '@aztec/foundation/branded-types'; import { chunkBy } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -12,7 +7,6 @@ import { sleep } from '@aztec/foundation/sleep'; import { MembershipWitness, type SiblingPath } from '@aztec/foundation/trees'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { - type BlockData, type BlockHash, type BlockParameter, type DataInBlock, @@ -187,39 +181,6 @@ export class NodeWorldStateQueries { return this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message); } - public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { - const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message); - if (messageIndex === undefined) { - return undefined; - } - // Post-flip, an L1-to-L2 message at compact leaf index `i` is consumed by the first block whose L1-to-L2 tree leaf - // count exceeds `i` (leaf counts are monotonic in block number); that block's checkpoint is the answer, sourced - // from the stored block records rather than 1024-per-checkpoint index arithmetic (AZIP-22 Fast Inbox). - const block = await this.#findBlockConsumingL1ToL2MessageIndex(messageIndex); - return block?.checkpointNumber; - } - - /** Binary-searches the block records for the first block whose L1-to-L2 tree leaf count exceeds `messageIndex`. */ - async #findBlockConsumingL1ToL2MessageIndex(messageIndex: bigint): Promise { - let lo: number = INITIAL_L2_BLOCK_NUM; - let hi: number = await this.blockSource.getBlockNumber(); - let result: BlockData | undefined; - while (lo <= hi) { - const mid = lo + Math.floor((hi - lo) / 2); - const block = await this.blockSource.getBlockData({ number: BlockNumber(mid) }); - if (block === undefined) { - break; - } - if (BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex) > messageIndex) { - result = block; - hi = mid - 1; - } else { - lo = mid + 1; - } - } - return result; - } - /** * Returns all the L2 to L1 messages in an epoch (empty array if the epoch is not found). The public * `AztecNodeService.getL2ToL1Messages` that delegates here is deprecated in favor of diff --git a/yarn-project/aztec.js/src/utils/cross_chain.test.ts b/yarn-project/aztec.js/src/utils/cross_chain.test.ts index 573c6453ce2a..839cc1a47c3d 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.test.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.test.ts @@ -1,4 +1,3 @@ -import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { BlockData } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; @@ -8,19 +7,20 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { isL1ToL2MessageReady } from './cross_chain.js'; describe('isL1ToL2MessageReady', () => { - let node: MockProxy>; + let node: MockProxy>; let messageHash: Fr; - const blockAtCheckpoint = (checkpointNumber: number) => - ({ checkpointNumber: CheckpointNumber(checkpointNumber) }) as BlockData; + /** A block whose L1-to-L2 message tree holds `leafCount` leaves, i.e. leaf indices 0..leafCount-1. */ + const blockWithMessageLeaves = (leafCount: number) => + ({ header: { state: { l1ToL2MessageTree: { nextAvailableLeafIndex: leafCount } } } }) as BlockData; beforeEach(() => { node = mock(); messageHash = Fr.random(); }); - it('returns false when the message is not yet in any checkpoint', async () => { - node.getL1ToL2MessageCheckpoint.mockResolvedValue(undefined); + it('returns false when the node has not seen the message yet', async () => { + node.getL1ToL2MessageIndex.mockResolvedValue(undefined); expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false); expect(node.getBlockData).not.toHaveBeenCalled(); @@ -28,24 +28,24 @@ describe('isL1ToL2MessageReady', () => { describe('latest fallback (no chain tip)', () => { beforeEach(() => { - node.getL1ToL2MessageCheckpoint.mockResolvedValue(CheckpointNumber(5)); + node.getL1ToL2MessageIndex.mockResolvedValue(5n); }); - it('checks readiness against the latest block', async () => { - node.getBlockData.mockResolvedValue(blockAtCheckpoint(5)); + it('returns true once the latest block has consumed the message leaf', async () => { + node.getBlockData.mockResolvedValue(blockWithMessageLeaves(6)); expect(await isL1ToL2MessageReady(node, messageHash)).toBe(true); expect(node.getBlockData).toHaveBeenCalledWith('latest'); }); - it('returns true once the latest block reaches the message checkpoint', async () => { - node.getBlockData.mockResolvedValue(blockAtCheckpoint(6)); + it('returns false when the latest block stops exactly at the message leaf', async () => { + node.getBlockData.mockResolvedValue(blockWithMessageLeaves(5)); - expect(await isL1ToL2MessageReady(node, messageHash)).toBe(true); + expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false); }); - it('returns false when the latest block is behind the message checkpoint', async () => { - node.getBlockData.mockResolvedValue(blockAtCheckpoint(4)); + it('returns false when the latest block is behind the message leaf', async () => { + node.getBlockData.mockResolvedValue(blockWithMessageLeaves(4)); expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false); }); @@ -59,13 +59,13 @@ describe('isL1ToL2MessageReady', () => { describe('with an explicit chain tip', () => { beforeEach(() => { - node.getL1ToL2MessageCheckpoint.mockResolvedValue(CheckpointNumber(5)); + node.getL1ToL2MessageIndex.mockResolvedValue(5n); }); it('compares against the requested tip instead of latest', async () => { - // The proven tip lags behind latest: the message is in checkpoint 5 but proven is only at 4. + // The proven tip lags behind latest: latest consumed the message leaf, proven has not. node.getBlockData.mockImplementation(param => - Promise.resolve(param === 'proven' ? blockAtCheckpoint(4) : blockAtCheckpoint(6)), + Promise.resolve(param === 'proven' ? blockWithMessageLeaves(5) : blockWithMessageLeaves(7)), ); expect(await isL1ToL2MessageReady(node, messageHash, 'latest')).toBe(true); @@ -73,9 +73,9 @@ describe('isL1ToL2MessageReady', () => { expect(node.getBlockData).toHaveBeenLastCalledWith('proven'); }); - it('returns true once the requested tip reaches the message checkpoint', async () => { + it('returns true once the requested tip has consumed the message leaf', async () => { node.getBlockData.mockImplementation(param => - Promise.resolve(param === 'proven' ? blockAtCheckpoint(5) : blockAtCheckpoint(7)), + Promise.resolve(param === 'proven' ? blockWithMessageLeaves(6) : blockWithMessageLeaves(8)), ); expect(await isL1ToL2MessageReady(node, messageHash, 'proven')).toBe(true); diff --git a/yarn-project/aztec.js/src/utils/cross_chain.ts b/yarn-project/aztec.js/src/utils/cross_chain.ts index 3010e6eab40a..9a0fec7dd9bd 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.ts @@ -10,7 +10,7 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; * @param opts - Options */ export function waitForL1ToL2MessageReady( - node: Pick, + node: Pick, l1ToL2MessageHash: Fr, opts: { /** Timeout for the operation in seconds */ timeoutSeconds: number; @@ -39,16 +39,17 @@ export function waitForL1ToL2MessageReady( * @returns True if the message is ready to be consumed, false otherwise */ export async function isL1ToL2MessageReady( - node: Pick, + node: Pick, l1ToL2MessageHash: Fr, chainTip: BlockTag = 'latest', ): Promise { - const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash); - if (messageCheckpointNumber === undefined) { + const messageIndex = await node.getL1ToL2MessageIndex(l1ToL2MessageHash); + if (messageIndex === undefined) { return false; } - // L1 to L2 messages are included in the first block of a checkpoint + // Blocks consume L1-to-L2 messages in Inbox order into consecutive leaves of the message tree, so the message is + // available at a tip exactly when that tip's tree has grown past the message's leaf index. const block = await node.getBlockData(chainTip); - return block !== undefined && block.checkpointNumber >= messageCheckpointNumber; + return block !== undefined && messageIndex < BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex); } diff --git a/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts b/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts index 098e033ad634..b484d886b69f 100644 --- a/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts +++ b/yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts @@ -434,12 +434,10 @@ describe('e2e_node_rpc_perf', () => { }); describe('message APIs', () => { - it('benchmarks getL1ToL2MessageCheckpoint', async () => { + it('benchmarks getL1ToL2MessageIndex', async () => { const l1ToL2Message = Fr.random(); - const { stats } = await benchmark('getL1ToL2MessageCheckpoint', () => - aztecNode.getL1ToL2MessageCheckpoint(l1ToL2Message), - ); - addResult('getL1ToL2MessageCheckpoint', stats); + const { stats } = await benchmark('getL1ToL2MessageIndex', () => aztecNode.getL1ToL2MessageIndex(l1ToL2Message)); + addResult('getL1ToL2MessageIndex', stats); expect(stats.avg).toBeLessThan(2000); }); diff --git a/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts b/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts index 9990541e8dda..87cfd0da01a0 100644 --- a/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts +++ b/yarn-project/end-to-end/src/composed/web3signer/e2e_multi_validator_node_key_store.test.ts @@ -376,7 +376,6 @@ describe('e2e_multi_validator_node', () => { blockHeader: BlockHeader, checkpointNumber: CheckpointNumber, indexWithinCheckpoint: number, - inHash: Fr, archive: Fr, txs: Tx[], proposerAddress: EthAddress | undefined, @@ -397,7 +396,6 @@ describe('e2e_multi_validator_node', () => { blockHeader, checkpointNumber, IndexWithinCheckpoint(indexWithinCheckpoint), - inHash, archive, txs, proposerAddress, diff --git a/yarn-project/end-to-end/src/fixtures/fixtures.ts b/yarn-project/end-to-end/src/fixtures/fixtures.ts index 217bc1ccf645..6bfd955cc976 100644 --- a/yarn-project/end-to-end/src/fixtures/fixtures.ts +++ b/yarn-project/end-to-end/src/fixtures/fixtures.ts @@ -30,8 +30,6 @@ export const PIPELINED_FEE_PADDING = TEST_FEE_PADDING; * * 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 @@ -44,7 +42,6 @@ export const PIPELINED_FEE_PADDING = TEST_FEE_PADDING; * - `walletMinFeePadding: PIPELINED_FEE_PADDING` (30x) to absorb the wider fee evolution window. */ export const PIPELINING_SETUP_OPTS = { - inboxLag: 2, minTxsPerBlock: 0, aztecSlotDuration: 12, ethereumSlotDuration: 4, @@ -75,7 +72,6 @@ export const PIPELINING_SETUP_OPTS = { */ export const AUTOMINE_E2E_OPTS = { useAutomineSequencer: true, - inboxLag: 1, minTxsPerBlock: 0, aztecSlotDuration: 12, ethereumSlotDuration: 4, diff --git a/yarn-project/end-to-end/src/multi-node/block-production/multi_validator_node.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/block-production/multi_validator_node.parallel.test.ts index 88c57c5d9197..4f1022ccc347 100644 --- a/yarn-project/end-to-end/src/multi-node/block-production/multi_validator_node.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/block-production/multi_validator_node.parallel.test.ts @@ -57,7 +57,6 @@ describe('multi-node/block-production/multi_validator_node', () => { anvilSlotsInAnEpoch: 4, blockDurationMs: 2000, minTxsPerBlock: 0, - inboxLag: 2, }); // All 5 validators in a single physical node. diff --git a/yarn-project/end-to-end/src/multi-node/block-production/redistribution.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/block-production/redistribution.parallel.test.ts index d2eeb78036b8..4f3dcfe90048 100644 --- a/yarn-project/end-to-end/src/multi-node/block-production/redistribution.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/block-production/redistribution.parallel.test.ts @@ -56,7 +56,6 @@ describe('multi-node/block-production/redistribution', () => { const test = await MultiNodeTestContext.setup({ numberOfAccounts: 0, initialValidators: validators, - inboxLag: 2, mockGossipSubNetwork: true, startProverNode: true, aztecEpochDuration: 4, diff --git a/yarn-project/end-to-end/src/multi-node/block-production/setup.ts b/yarn-project/end-to-end/src/multi-node/block-production/setup.ts index 595ae29cdea5..802d3f12f2de 100644 --- a/yarn-project/end-to-end/src/multi-node/block-production/setup.ts +++ b/yarn-project/end-to-end/src/multi-node/block-production/setup.ts @@ -171,7 +171,6 @@ export async function setupBlockProductionWithProver(opts: { ...setupOpts, pxeOpts: { syncChainTip }, skipInitialSequencer: true, - inboxLag: 2, }, nodeOpts: (index: number) => ({ dontStartSequencer: true, diff --git a/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts b/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts index 59ff485e4c5d..4f6c25d556c6 100644 --- a/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/multi-node/governance/add_rollup.test.ts @@ -89,11 +89,8 @@ describe('multi-node/governance/add_rollup', () => { aztecTargetCommitteeSize: NUM_VALIDATORS, governanceProposerRoundSize: 10, // Allow validators to build empty checkpoints so the chain keeps advancing while we wait for - // L1->L2 messages to land in the next checkpoint's inbox tree. + // L1->L2 messages to be consumed from the streaming Inbox. minTxsPerBlock: 0, - // inboxLag: 2 sources L1->L2 messages from an already-sealed checkpoint under pipelining, avoiding - // L1ToL2MessagesNotReadyError. - inboxLag: 2, // Fund the bridging accounts (and the sponsored FPC) at genesis. Skip the hardcoded-account // fast-path so our additionallyFundedAccounts are not clobbered. skipHardcodedAccount: true, @@ -160,7 +157,6 @@ describe('multi-node/governance/add_rollup', () => { aztecTargetCommitteeSize: context.aztecNodeConfig.aztecTargetCommitteeSize, lagInEpochsForValidatorSet: context.aztecNodeConfig.lagInEpochsForValidatorSet, lagInEpochsForRandao: context.aztecNodeConfig.lagInEpochsForRandao, - inboxLag: context.aztecNodeConfig.inboxLag, aztecProofSubmissionEpochs: context.aztecNodeConfig.aztecProofSubmissionEpochs, slashingQuorum: context.aztecNodeConfig.slashingQuorum, slashingRoundSizeInEpochs: context.aztecNodeConfig.slashingRoundSizeInEpochs, diff --git a/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts index 36a6dd07bf23..d81717bb62b1 100644 --- a/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/multi-node/governance/upgrade_governance_proposer.test.ts @@ -38,7 +38,6 @@ describe('multi-node/governance/upgrade_governance_proposer', () => { governanceProposerRoundSize: 10, activationThreshold: 10n ** 22n, ejectionThreshold: 5n ** 22n, - inboxLag: 2, minTxsPerBlock: 0, initialValidators: buildMockGossipValidators(NUM_VALIDATORS), }); diff --git a/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_block_proposal_slash.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_block_proposal_slash.test.ts index d1876afe573e..2a97f16d648f 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_block_proposal_slash.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_block_proposal_slash.test.ts @@ -49,7 +49,6 @@ describe('multi-node/slashing/broadcasted_invalid_block_proposal_slash', () => { sentinelEnabled: false, // reuse only the fast 8s-slot timing; this test does not use the sentinel blockDurationMs: 2000, aztecTargetCommitteeSize: COMMITTEE_SIZE, - inboxLag: 2, aztecProofSubmissionEpochs: 1024, // effectively do not reorg slashInactivityConsecutiveEpochThreshold: 32, // effectively do not slash for inactivity minTxsPerBlock: 0, // always be building diff --git a/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_checkpoint_proposal_slash.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_checkpoint_proposal_slash.parallel.test.ts index b207200e5a0a..89e9d8da96b4 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_checkpoint_proposal_slash.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/broadcasted_invalid_checkpoint_proposal_slash.parallel.test.ts @@ -240,7 +240,6 @@ describe('multi-node/slashing/broadcasted_invalid_checkpoint_proposal_slash', () aztecTargetCommitteeSize: COMMITTEE_SIZE, aztecProofSubmissionEpochs: 1024, minTxsPerBlock: 0, - inboxLag: 2, slashingQuorum: SLASHING_QUORUM, slashingRoundSizeInEpochs: SLASHING_ROUND_SIZE / SENTINEL_TIMING.aztecEpochDuration, slashAmountSmall: slashingUnit, diff --git a/yarn-project/end-to-end/src/multi-node/slashing/data_withholding_slash.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/data_withholding_slash.test.ts index ea146ea02982..da03696d2fa2 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/data_withholding_slash.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/data_withholding_slash.test.ts @@ -91,7 +91,6 @@ describe('multi-node/slashing/data_withholding_slash', () => { slashDataWithholdingToleranceSlots: TOLERANCE_SLOTS, slashDataWithholdingPenalty: slashingAmount, minTxsPerBlock: 1, - inboxLag: 2, initialValidators: buildMockGossipValidators(NUM_VALIDATORS), }); }); diff --git a/yarn-project/end-to-end/src/multi-node/slashing/inactivity_setup.ts b/yarn-project/end-to-end/src/multi-node/slashing/inactivity_setup.ts index beec45ff503b..073e28f94955 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/inactivity_setup.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/inactivity_setup.ts @@ -48,7 +48,6 @@ export class InactivityTest { private async run(opts: { slashInactivityConsecutiveEpochThreshold: number; inactiveNodeCount: number }) { this.test = await MultiNodeTestContext.setup({ ...SLASHER_ENABLED_MULTI_VALIDATOR_OPTS, - inboxLag: 2, anvilSlotsInAnEpoch: 4, // A fake prover node is started by the context (realProofs:false); give it the multi-epoch // proving delay the inactivity scenario relied on, and keep enough broker history. diff --git a/yarn-project/end-to-end/src/multi-node/slashing/multiple_validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/multiple_validators_sentinel.parallel.test.ts index 8bfb047e9d5a..df1d4cc42312 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/multiple_validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/multiple_validators_sentinel.parallel.test.ts @@ -47,7 +47,6 @@ describe('multi-node/slashing/multiple_validators_sentinel', () => { minTxsPerBlock: 0, slashingRoundSizeInEpochs: 2, slashInactivityPenalty: 0n, // Set to 0 to disable - inboxLag: 2, initialValidators: buildMockGossipValidators(NUM_VALIDATORS), }); diff --git a/yarn-project/end-to-end/src/multi-node/slashing/sentinel_status_slash.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/sentinel_status_slash.parallel.test.ts index 47b64bf660f9..d2f14d5c46a5 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/sentinel_status_slash.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/sentinel_status_slash.parallel.test.ts @@ -73,7 +73,6 @@ describe('multi-node/slashing/sentinel_status_slash', () => { blockDurationMs: 2000, aztecProofSubmissionEpochs: 1024, minTxsPerBlock: 0, - inboxLag: 2, // A single proposer-fault slot in an epoch gives missed/total = 1/6 ≈ 0.167; threshold // 0.1 lets that single fault trip inactivity. slashInactivityTargetPercentage: 0.1, diff --git a/yarn-project/end-to-end/src/multi-node/slashing/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/slash_veto_demo.test.ts index 6ab8d440b5cf..fe2c35371c34 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/slash_veto_demo.test.ts @@ -71,7 +71,6 @@ describe('veto slash', () => { blockDurationMs: 2000, aztecProofSubmissionEpochs: 1024, // effectively do not reorg minTxsPerBlock: 0, - inboxLag: 2, aztecTargetCommitteeSize: NUM_VALIDATORS, slashSelfAllowed: true, slashingOffsetInRounds: SLASH_OFFSET_IN_ROUNDS, diff --git a/yarn-project/end-to-end/src/multi-node/slashing/validators_sentinel.parallel.test.ts b/yarn-project/end-to-end/src/multi-node/slashing/validators_sentinel.parallel.test.ts index 09c9a6b1870e..fd40dafb2e8a 100644 --- a/yarn-project/end-to-end/src/multi-node/slashing/validators_sentinel.parallel.test.ts +++ b/yarn-project/end-to-end/src/multi-node/slashing/validators_sentinel.parallel.test.ts @@ -42,7 +42,6 @@ describe('multi-node/slashing/validators_sentinel', () => { minTxsPerBlock: 0, slashingRoundSizeInEpochs: 2, slashInactivityPenalty: 0n, // Set to 0 to disable - inboxLag: 2, initialValidators: buildMockGossipValidators(NUM_VALIDATORS), }); diff --git a/yarn-project/end-to-end/src/p2p/fee_asset_price_oracle_gossip.test.ts b/yarn-project/end-to-end/src/p2p/fee_asset_price_oracle_gossip.test.ts index 21c010085ac3..5e4b1b15fc87 100644 --- a/yarn-project/end-to-end/src/p2p/fee_asset_price_oracle_gossip.test.ts +++ b/yarn-project/end-to-end/src/p2p/fee_asset_price_oracle_gossip.test.ts @@ -46,9 +46,6 @@ describe('e2e_p2p_network', () => { slashingRoundSizeInEpochs: 2, slashingQuorum: 5, listenAddress: '127.0.0.1', - // Pipelining: target-slot is one ahead of build-slot; inboxLag sources L1->L2 - // messages from the previous checkpoint to avoid L1ToL2MessagesNotReadyError. - inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/p2p/gossip_network.test.ts index a1df69dce8cd..46f7409e5515 100644 --- a/yarn-project/end-to-end/src/p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/p2p/gossip_network.test.ts @@ -65,7 +65,6 @@ describe('e2e_p2p_network', () => { slashingRoundSizeInEpochs: 2, slashingQuorum: 5, listenAddress: '127.0.0.1', - inboxLag: 2, }, }); @@ -160,7 +159,6 @@ describe('e2e_p2p_network', () => { // Without this, no blocks are built until txs arrive, and a failed checkpoint during tx // submission causes block pruning that invalidates tx references. minTxsPerBlock: 0, - inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/p2p/late_prover_tx_collection.test.ts b/yarn-project/end-to-end/src/p2p/late_prover_tx_collection.test.ts index 0c46f4693c49..2baea1ce1db1 100644 --- a/yarn-project/end-to-end/src/p2p/late_prover_tx_collection.test.ts +++ b/yarn-project/end-to-end/src/p2p/late_prover_tx_collection.test.ts @@ -52,7 +52,6 @@ describe('e2e_p2p_late_prover_tx_collection', () => { // Only build blocks that actually carry txs, so the chain idles after our block is mined and // the late prover is never auto-triggered to collect for a different block. minTxsPerBlock: 1, - inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts b/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts index b2242e916abb..d1f1c64f21e8 100644 --- a/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts +++ b/yarn-project/end-to-end/src/p2p/preferred_gossip_network.test.ts @@ -136,7 +136,6 @@ describe('e2e_p2p_preferred_network', () => { // Just for testing be aggressive here, don't allow any auth handshake failures p2pMaxFailedAuthAttemptsAllowed: 0, minTxsPerBlock: 0, - inboxLag: 2, }, }); diff --git a/yarn-project/end-to-end/src/p2p/rediscovery.test.ts b/yarn-project/end-to-end/src/p2p/rediscovery.test.ts index 7eed53674dd9..f44c24eff919 100644 --- a/yarn-project/end-to-end/src/p2p/rediscovery.test.ts +++ b/yarn-project/end-to-end/src/p2p/rediscovery.test.ts @@ -33,7 +33,6 @@ describe('e2e_p2p_rediscovery', () => { aztecSlotDuration: 24, blockDurationMs: BLOCK_DURATION_MS, listenAddress: '127.0.0.1', - inboxLag: 2, }, }); await t.setup(); diff --git a/yarn-project/end-to-end/src/p2p/reqresp/utils.ts b/yarn-project/end-to-end/src/p2p/reqresp/utils.ts index e8e69cd7902c..535d884c9c14 100644 --- a/yarn-project/end-to-end/src/p2p/reqresp/utils.ts +++ b/yarn-project/end-to-end/src/p2p/reqresp/utils.ts @@ -43,9 +43,6 @@ export async function createReqrespTest(options: ReqrespOptions = {}): Promise

L2 - // messages from the previous checkpoint to avoid L1ToL2MessagesNotReadyError. - inboxLag: 2, }, }); await t.setup(); diff --git a/yarn-project/end-to-end/src/single-node/block-building/debug_trace.test.ts b/yarn-project/end-to-end/src/single-node/block-building/debug_trace.test.ts index fcb86aab36b1..99edbf848904 100644 --- a/yarn-project/end-to-end/src/single-node/block-building/debug_trace.test.ts +++ b/yarn-project/end-to-end/src/single-node/block-building/debug_trace.test.ts @@ -59,7 +59,6 @@ describe('single-node/block-building/debug_trace', () => { // enough to reach it, where the proposer selection changes and the propose silently reverts. aztecEpochDuration: 32, aztecProofSubmissionEpochs: NO_REORG_SUBMISSION_EPOCHS, - inboxLag: 2, }); ({ aztecNode, logger, aztecNodeAdmin, config } = test.context); sequencer = test.context.sequencer! as TestSequencerClient; diff --git a/yarn-project/end-to-end/src/single-node/cross-chain/message_test_helpers.ts b/yarn-project/end-to-end/src/single-node/cross-chain/message_test_helpers.ts index afacbaf22788..0233eaa098a4 100644 --- a/yarn-project/end-to-end/src/single-node/cross-chain/message_test_helpers.ts +++ b/yarn-project/end-to-end/src/single-node/cross-chain/message_test_helpers.ts @@ -4,7 +4,7 @@ import type { Logger } from '@aztec/aztec.js/log'; import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; -import type { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import type { BlockNumber } from '@aztec/foundation/branded-types'; import { retryUntil } from '@aztec/foundation/retry'; import { ExecutionPayload } from '@aztec/stdlib/tx'; @@ -33,7 +33,7 @@ export interface L1ToL2MessageHelpers { secretHash: Fr; }): ReturnType; advanceBlock(): Promise; - waitForMessageFetched(msgHash: Fr): Promise; + waitForMessageIndexed(msgHash: Fr): Promise; waitForMessageReady( msgHash: Fr, scope: L1ToL2MessageScope, @@ -72,20 +72,20 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL return newBlock; }; - // Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint. - // Advances a block on each retry because an L1->L2 message is only indexed once further L2 blocks build. - const waitForMessageFetched = async (msgHash: Fr) => { + // Waits until the node's archiver has ingested the message from the Inbox and returns its message-tree leaf index. + // Advances a block on each retry to keep the chain moving while the archiver catches up with L1. + const waitForMessageIndexed = async (msgHash: Fr) => { log.warn(`Waiting until the message is fetched by the node`); return await retryUntil( async () => { - const checkpoint = await aztecNode.getL1ToL2MessageCheckpoint(msgHash); - if (checkpoint !== undefined) { - return checkpoint; + const messageIndex = await aztecNode.getL1ToL2MessageIndex(msgHash); + if (messageIndex !== undefined) { + return messageIndex; } await advanceBlock(); return undefined; }, - 'get msg checkpoint', + 'get msg index', 60, ); }; @@ -96,9 +96,9 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL scope: L1ToL2MessageScope, onNotReady?: (blockNumber: BlockNumber) => Promise, ) => { - const msgCheckpoint = await waitForMessageFetched(msgHash); + const msgIndex = await waitForMessageIndexed(msgHash); log.warn( - `Waiting until L2 reaches the first block of msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getCheckpointNumber()})`, + `Waiting until L2 consumes msg leaf index ${msgIndex} (checkpoint is ${await aztecNode.getCheckpointNumber()})`, ); await retryUntil( async () => { @@ -109,17 +109,17 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL const witness = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash); const isReady = await isL1ToL2MessageReady(aztecNode, msgHash, t.pxeSyncChainTip); log.info( - `Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message checkpoint is ${msgCheckpoint}. Witness ${!!witness}. Ready ${isReady}.`, + `Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message leaf index is ${msgIndex}. Witness ${!!witness}. Ready ${isReady}.`, ); if (!isReady) { await (onNotReady ? onNotReady(blockNumber) : advanceBlock()); } return isReady; }, - `wait for rollup to reach msg checkpoint ${msgCheckpoint}`, + `wait for rollup to consume msg leaf index ${msgIndex}`, 240, ); }; - return { sendMessageToL2, advanceBlock, waitForMessageFetched, waitForMessageReady }; + return { sendMessageToL2, advanceBlock, waitForMessageIndexed, waitForMessageReady }; } diff --git a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts index 53b062cff8ce..79bc06be39bf 100644 --- a/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts +++ b/yarn-project/end-to-end/src/single-node/fees/fee_settings.test.ts @@ -76,7 +76,6 @@ describe('single-node/fees/fee_settings', () => { // (Test-body txs explicitly call `wallet.setMinFeePadding(...)` so they don't use the wallet default.) const AZTEC_SLOT_DURATION = 12; const t = new FeesTest('fee_juice', 1, { - inboxLag: 2, minTxsPerBlock: 0, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: 4, diff --git a/yarn-project/end-to-end/src/single-node/sequencer/escape_hatch_vote_only.test.ts b/yarn-project/end-to-end/src/single-node/sequencer/escape_hatch_vote_only.test.ts index f85cd95d6f76..0fb0454b1353 100644 --- a/yarn-project/end-to-end/src/single-node/sequencer/escape_hatch_vote_only.test.ts +++ b/yarn-project/end-to-end/src/single-node/sequencer/escape_hatch_vote_only.test.ts @@ -93,7 +93,6 @@ describe('single-node/sequencer/escape_hatch_vote_only', () => { 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. - inboxLag: 2, }); ({ diff --git a/yarn-project/end-to-end/src/single-node/single_node_test_context.ts b/yarn-project/end-to-end/src/single-node/single_node_test_context.ts index 2e73c4fee273..00285605b4dc 100644 --- a/yarn-project/end-to-end/src/single-node/single_node_test_context.ts +++ b/yarn-project/end-to-end/src/single-node/single_node_test_context.ts @@ -265,7 +265,6 @@ export class SingleNodeTestContext { slasherEnabled: false, // `inboxLag: 2` is the intended value when running with pipelining (the production config // default of 1 is a separate bug). Set before `...opts` so tests can still override. - inboxLag: 2, ...opts, ...(hardcodedAccountData ? { additionallyFundedAccounts: [hardcodedAccountData], numberOfAccounts: 0 } : {}), }, diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts index 8388a5c74a54..5d8fa61cceb2 100644 --- a/yarn-project/ethereum/src/config.ts +++ b/yarn-project/ethereum/src/config.ts @@ -35,8 +35,6 @@ export type L1ContractsConfig = { lagInEpochsForValidatorSet: number; /** The number of epochs to lag behind the current epoch for randao selection. */ lagInEpochsForRandao: number; - /** The number of checkpoints to lag in the inbox (prevents sequencer DOS attacks). */ - inboxLag: number; /** The number of epochs after an epoch ends that proofs are still accepted. */ aztecProofSubmissionEpochs: number; /** The deposit amount for a validator */ @@ -129,11 +127,6 @@ export const l1ContractsConfigMappings: ConfigMappingsType = description: 'The number of epochs to lag behind the current epoch for randao selection.', ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_LAG_IN_EPOCHS_FOR_RANDAO), }, - inboxLag: { - env: 'AZTEC_INBOX_LAG', - description: 'The number of checkpoints to lag in the inbox (prevents sequencer DOS attacks).', - ...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_INBOX_LAG), - }, aztecProofSubmissionEpochs: { env: 'AZTEC_PROOF_SUBMISSION_EPOCHS', description: 'The number of epochs after an epoch ends that proofs are still accepted.', diff --git a/yarn-project/ethereum/src/contracts/inbox.ts b/yarn-project/ethereum/src/contracts/inbox.ts index 70ce019de4d8..1960962a7fc5 100644 --- a/yarn-project/ethereum/src/contracts/inbox.ts +++ b/yarn-project/ethereum/src/contracts/inbox.ts @@ -1,6 +1,6 @@ import { asyncPool } from '@aztec/foundation/async-pool'; import { maxBigint } from '@aztec/foundation/bigint'; -import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; +import { Buffer32 } from '@aztec/foundation/buffer'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { InboxAbi } from '@aztec/l1-artifacts/InboxAbi'; @@ -18,11 +18,9 @@ import { checkBlockTag } from './utils.js'; export type MessageSentArgs = { index: bigint; leaf: Fr; - /** Legacy 128-bit keccak rolling hash of all messages inserted up to and including this one. */ - rollingHash: Buffer16; - /** Consensus rolling hash (truncated sha256 chain) after this message (AZIP-22 Fast Inbox). */ + /** Consensus rolling hash (truncated sha256 chain) after this message. */ inboxRollingHash: Fr; - /** Sequence number of the Inbox bucket this message was absorbed into (AZIP-22 Fast Inbox). */ + /** Sequence number of the Inbox bucket this message was absorbed into. */ bucketSeq: bigint; }; @@ -72,10 +70,41 @@ export class InboxContract { const state = await this.inbox.read.getState(opts); return { totalMessagesInserted: state.totalMessagesInserted, - messagesRollingHash: Buffer16.fromString(state.rollingHash), }; } + /** Returns the sequence number of the Inbox bucket currently accumulating messages. */ + public async getCurrentBucketSeq(opts: { blockTag?: BlockTag; blockNumber?: bigint } = {}): Promise { + await checkBlockTag(opts.blockNumber, this.client); + return this.inbox.read.getCurrentBucketSeq(opts); + } + + /** Returns the Inbox bucket with the given sequence number. */ + public async getBucket( + seq: bigint, + opts: { blockTag?: BlockTag; blockNumber?: bigint } = {}, + ): Promise { + await checkBlockTag(opts.blockNumber, this.client); + const bucket = await this.inbox.read.getBucket([seq], opts); + return { + rollingHash: Fr.fromString(bucket.rollingHash), + totalMsgCount: bucket.totalMsgCount, + timestamp: bucket.timestamp, + msgCount: bucket.msgCount, + }; + } + + /** + * Returns the Inbox bucket currently accumulating messages: its consensus rolling hash and cumulative message + * total are the Inbox's live chain position, used by the archiver's message sync and L1-reorg detection. + */ + public async getCurrentBucket( + opts: { blockTag?: BlockTag; blockNumber?: bigint } = {}, + ): Promise { + const seq = await this.getCurrentBucketSeq(opts); + return this.getBucket(seq, opts); + } + /** Fetches MessageSent events within the given block range. */ async getMessageSentEvents(fromBlock: bigint, toBlock: bigint): Promise { const logs = (await this.inbox.getEvents.MessageSent({}, { fromBlock, toBlock })).filter( @@ -128,7 +157,6 @@ export class InboxContract { args: { index?: bigint; hash?: `0x${string}`; - rollingHash?: `0x${string}`; inboxRollingHash?: `0x${string}`; bucketSeq?: bigint; }; @@ -143,7 +171,6 @@ export class InboxContract { args: { index: log.args.index!, leaf: Fr.fromString(log.args.hash!), - rollingHash: Buffer16.fromString(log.args.rollingHash!), inboxRollingHash: Fr.fromString(log.args.inboxRollingHash!), bucketSeq: log.args.bucketSeq!, }, @@ -153,10 +180,16 @@ export class InboxContract { export type InboxContractState = { totalMessagesInserted: bigint; - messagesRollingHash: Buffer16; - /** - * Checkpoint currently accumulating messages, when known. No longer tracked on-chain post-flip (AZIP-22 Fast - * Inbox); used only by the legacy per-checkpoint message-readiness gate. - */ - treeInProgress?: bigint; +}; + +/** A snapshot of an on-chain Inbox rolling-hash bucket. */ +export type InboxContractBucket = { + /** Consensus rolling hash (truncated sha256 chain) after the last message absorbed into this bucket. */ + rollingHash: Fr; + /** Cumulative number of messages inserted into the Inbox up to and including this bucket. */ + totalMsgCount: bigint; + /** L1 block timestamp at which this bucket was opened; its recency key, in seconds. */ + timestamp: bigint; + /** Number of messages absorbed into this bucket. */ + msgCount: number; }; diff --git a/yarn-project/ethereum/src/queries.ts b/yarn-project/ethereum/src/queries.ts index ca5a7cfdcc02..be3ea3663823 100644 --- a/yarn-project/ethereum/src/queries.ts +++ b/yarn-project/ethereum/src/queries.ts @@ -77,7 +77,7 @@ export async function getL1ContractsConfig( publicClient: ViemPublicClient, addresses: { governanceAddress: EthAddress; rollupAddress?: EthAddress }, ): Promise< - Omit & { + Omit & { l1StartBlock: bigint; l1GenesisTime: bigint; rollupVersion: number; diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 09c3eacc19cc..418950675b78 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -323,7 +323,6 @@ export type EnvVar = | 'AZTEC_TARGET_COMMITTEE_SIZE' | 'AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET' | 'AZTEC_LAG_IN_EPOCHS_FOR_RANDAO' - | 'AZTEC_INBOX_LAG' | 'AZTEC_PROOF_SUBMISSION_EPOCHS' | 'AZTEC_ACTIVATION_THRESHOLD' | 'AZTEC_EJECTION_THRESHOLD' diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index 1175be199df8..2172e68847db 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -81,7 +81,10 @@ export async function createP2PClient( 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); + // Attestation store version 3: persisted proposal/attestation buffers embed the checkpoint header (which lost + // `inHash`) and the block-proposal wire format (which dropped its zeroed `inHash`), so pre-Fast-Inbox bytes no + // longer decode. Bumped to wipe stale pools; same no-migration policy as the archiver store. + const attestationStore = await createStore(P2P_ATTESTATION_STORE_NAME, 3, config, bindings); const l1Constants = await archiver.getL1Constants(); const rollupAddress = inputConfig.rollupAddress.toString().toLowerCase().replace(/^0x/, ''); 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 3c163f0fe551..30ea05fe0d31 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -20,7 +20,7 @@ import { makeCheckpointProposal, mockTx, } from '@aztec/stdlib/testing'; -import { TxArray, TxHashArray } from '@aztec/stdlib/tx'; +import { TxArray, TxHash, TxHashArray } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import { ServerWorldStateSynchronizer } from '@aztec/world-state'; @@ -809,8 +809,8 @@ describe('LibP2PService', () => { }); // 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. + // signed field (e.g. the transaction set) used to dedup by archive only and silently drop the + // second one. The pool now dedups by signed-payload hash, so the equivocation surfaces. it('same archive but different signed payload triggers slash callback', async () => { const blockHeader = makeBlockHeader(1, { slotNumber: targetSlot }); const indexWithinCheckpoint = IndexWithinCheckpoint(0); @@ -820,7 +820,7 @@ describe('LibP2PService', () => { signer, blockHeader, indexWithinCheckpoint, - inHash: Fr.fromString('0x1'), + txHashes: [TxHash.fromField(new Fr(1))], archiveRoot: sharedArchive, }); await service.processBlockFromPeer(proposal1.toBuffer(), 'msg-1', mockPeerId); @@ -830,7 +830,7 @@ describe('LibP2PService', () => { signer, blockHeader, indexWithinCheckpoint, - inHash: Fr.fromString('0x2'), + txHashes: [TxHash.fromField(new Fr(2))], archiveRoot: sharedArchive, }); expect(proposal2.archive.toString()).toBe(proposal1.archive.toString()); diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 28b021859fe1..f0760d4befd6 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -705,7 +705,6 @@ describe('ProverNode', () => { */ function setupRegistrationSuccess() { worldState.syncImmediate.mockResolvedValue(undefined as any); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); l2BlockSource.getBlockData.mockResolvedValue({ header: { lastArchive: { root: Fr.ZERO }, state: { l1ToL2MessageTree: { nextAvailableLeafIndex: 0 } } }, } as any); diff --git a/yarn-project/sequencer-client/README.md b/yarn-project/sequencer-client/README.md index 5b709095ce54..d9145611c527 100644 --- a/yarn-project/sequencer-client/README.md +++ b/yarn-project/sequencer-client/README.md @@ -142,7 +142,7 @@ Inside `execute()`: - Transitions to `INITIALIZING_CHECKPOINT`. If there is a pending invalidation, enqueue it. - Builds **pipelined-parent simulation overrides**: when building on top of a parent that hasn't landed on L1 yet, the fee-asset price modifier must be computed against the parent fee header we predicted (not the L1 one), so all in-flight checkpoints in the pipeline agree on the same modifier. - Asks the global variables builder for the slot's `CheckpointGlobalVariables` (`coinbase`, `feeRecipient`, `timestamp`, `gasFees`, `chainId`, `version`, `slotNumber`). These are shared across every block within the checkpoint — only `blockNumber` increments. - - Computes `inHash` from the L1→L2 messages for the checkpoint, and collects `previousCheckpointOutHashes` for prior checkpoints in the same epoch. + - Resolves the streaming Inbox consumption cursor (the parent checkpoint's last-consumed bucket, from the fork's L1→L2 leaf count), and collects `previousCheckpointOutHashes` for prior checkpoints in the same epoch. Each block then selects its own message bundle against the cursor (AZIP-22 Fast Inbox). - Forks world state at the parent (`closeDelayMs: 12 s`) and asks `FullNodeCheckpointsBuilder` for a `CheckpointBuilder` bound to that fork. - Runs `buildBlocksForCheckpoint()` — the per-block loop, described below. - Transitions to `ASSEMBLING_CHECKPOINT`, asks the builder to `completeCheckpoint()`, validates it against the configured caps, and asks the validator client to sign the `CheckpointProposal` (which bundles the final block proposal so the two travel together). @@ -206,7 +206,7 @@ See the [Block Building Timetable Spec](../stdlib/src/timetable/README.md) for t Key entry points: - `canProposeAt(archive, msgSender, simulationOverridesPlan?)` — eth_call simulation of `Rollup.canProposeAt`. The sequencer runs this before deciding to build. -- `enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, opts)` — adds the propose call, with a `preCheck` that re-validates the proposal against real L1 state when it is finally sent (catches drift between build time and submit time). +- `enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, bucketHint, opts)` — adds the propose call, with a `preCheck` that re-validates the proposal against real L1 state when it is finally sent (catches drift between build time and submit time). - `enqueueInvalidateCheckpoint`, `enqueueGovernanceCastSignal`, `enqueueSlashingActions` — the rest of the actions a proposer may bundle. - `sendRequests(targetSlot?)` — immediately flushes the queue as one Multicall3 transaction. Used by the `AutomineSequencer` for synchronous in-slot publishing. - `sendRequestsAt(targetSlot)` — the production (pipelined) path: sleeps (cancellable) until the ideal L1 send time, runs each request's `preCheck` (dropping those that fail), and then calls `sendRequests(targetSlot)`, which filters expired requests, sorts the remainder, and submits one Multicall3 that mines inside the target slot. diff --git a/yarn-project/sequencer-client/src/index.ts b/yarn-project/sequencer-client/src/index.ts index 2375b9013a90..c88e2a5f5d74 100644 --- a/yarn-project/sequencer-client/src/index.ts +++ b/yarn-project/sequencer-client/src/index.ts @@ -6,3 +6,10 @@ export { Sequencer, SequencerState, type SequencerEvents } from './sequencer/ind // Used by the node to simulate public parts of transactions. Should these be moved to a shared library? // ISSUE(#9832) export * from './global_variable_builder/index.js'; +export { + type ConsumedBucketCursor, + type InboxBucketSelection, + type InboxBucketSource, + type SelectInboxBucketInput, + selectInboxBucketForBlock, +} from './sequencer/inbox_bucket_selector.js'; diff --git a/yarn-project/sequencer-client/src/publisher/l1_publisher.integration.test.ts b/yarn-project/sequencer-client/src/publisher/l1_publisher.integration.test.ts index 05f09d1745e5..a2b482cf14a2 100644 --- a/yarn-project/sequencer-client/src/publisher/l1_publisher.integration.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1_publisher.integration.test.ts @@ -116,7 +116,7 @@ const logger = createLogger('integration_l1_publisher'); const config: SequencerClientConfig & L1ContractsConfig = { ...getL1ContractsConfigEnvVars(), ...getConfigEnvVars() }; // Several consecutive checkpoints, each consuming the L1->L2 messages sent while it was being built, so real -// messages are genuinely consumed and validated on L1 (AZIP-22 Fast Inbox). +// messages are genuinely consumed and validated on L1. const numberOfConsecutiveBlocks = 3; jest.setTimeout(1000000); @@ -144,7 +144,7 @@ describe('L1Publisher integration', () => { let builderDb: NativeWorldStateService; // Backs the blockSource mock's streaming L1->L2 message queries. The world-state synchronizer reconstructs each - // block's consumed message bundle from Inbox buckets (AZIP-22 Fast Inbox) when it syncs a block back, so the test + // block's consumed message bundle from Inbox buckets when it syncs a block back, so the test // registers one bucket per published block here (see buildAndPublishBlock). let messageSource: MockL1ToL2MessageSource; @@ -373,7 +373,7 @@ describe('L1Publisher integration', () => { getBlockNumber(): Promise { return Promise.resolve(BlockNumber(blocks.at(-1)?.number ?? BlockNumber.ZERO)); }, - // Streaming L1->L2 message reconstruction (AZIP-22 Fast Inbox): the world-state synchronizer resolves each + // Streaming L1->L2 message reconstruction: the world-state synchronizer resolves each // block's consumed message bundle from the Inbox buckets registered per published block in buildAndPublishBlock. getInboxBucketByTotalMsgCount(totalMsgCount: bigint) { return messageSource.getInboxBucketByTotalMsgCount(totalMsgCount); @@ -552,7 +552,7 @@ describe('L1Publisher integration', () => { describe('block building', () => { beforeEach(async () => { // This suite proposes consecutive checkpoints, each consuming the streaming-Inbox messages sent while it was - // being built (AZIP-22 Fast Inbox), so real messages are genuinely consumed and validated on L1. + // being built, so real messages are genuinely consumed and validated on L1. await setup(); }); @@ -567,7 +567,7 @@ describe('L1Publisher integration', () => { '0x1647b194c649f5dd01d7c832f89b0f496043c9150797923ea89e93d5ac619a93', ); - // Streaming Inbox consumption (AZIP-22 Fast Inbox): the L1 Rollup only lets a checkpoint consume Inbox buckets + // Streaming Inbox consumption: the L1 Rollup only lets a checkpoint consume Inbox buckets // that have aged past the censorship cutoff (`toTimestamp(slot - 1) - INBOX_LAG_SECONDS`), measured in L1 time, // not whole checkpoints. Each checkpoint mirrors the real Inbox buckets into messageSource, then reuses the // production `selectInboxBucketForBlock` (which mirrors `ProposeLib.validateInboxConsumption`) to pick exactly 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 8cdddbdf0d9e..27af8fd3cf2d 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 @@ -249,7 +249,6 @@ describe('CheckpointProposalJob', () => { checkpointBuilder = checkpointsBuilder.createCheckpointBuilder(checkpointConstants, checkpointNumber); l1ToL2MessageSource = mock(); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(Array(4).fill(Fr.ZERO)); // Genesis bucket for the empty-tree cursor above; with no newer synced buckets mocked, block bundle // selection consumes nothing by default. l1ToL2MessageSource.getInboxBucketByTotalMsgCount.mockResolvedValue({ @@ -297,12 +296,11 @@ describe('CheckpointProposalJob', () => { validatorClient = mock(); validatorClient.collectAttestations.mockImplementation(() => Promise.resolve([])); validatorClient.createBlockProposal.mockImplementation( - async (blockHeader, _checkpointNumber, indexWithinCheckpoint, inHash, archiveRoot, txs) => { + async (blockHeader, _checkpointNumber, indexWithinCheckpoint, archiveRoot, txs) => { const txHashes = await Promise.all((txs ?? []).map((tx: Tx) => tx.getTxHash())); return new BlockProposal( blockHeader, IndexWithinCheckpoint(indexWithinCheckpoint), - inHash, archiveRoot, txHashes, mockedSig, @@ -1445,8 +1443,7 @@ describe('CheckpointProposalJob', () => { expect(checkpoint).toBeDefined(); - // Streaming skips the bulk per-checkpoint fetch; every message reaches the builder through a block. - expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled(); + // Streaming has no bulk per-checkpoint list; every message reaches the builder through a block. expect(checkpointsBuilder.startCheckpointCalls).toHaveLength(1); // The first block consumes the selected bundle; the second consumes nothing. @@ -1455,7 +1452,7 @@ describe('CheckpointProposalJob', () => { expect(checkpointBuilder.buildBlockCalls[1].opts.l1ToL2Messages).toEqual([]); // Both block proposals carry the selected bucket reference (the second reuses the first's). - const bucketRefArgs = validatorClient.createBlockProposal.mock.calls.map(call => call[8]); + const bucketRefArgs = validatorClient.createBlockProposal.mock.calls.map(call => call[7]); expect(bucketRefArgs).toHaveLength(2); expect(bucketRefArgs[0]?.bucketSeq).toBe(2n); expect(bucketRefArgs[1]?.bucketSeq).toBe(2n); 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 558059f6fd2e..a11d3f131727 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 @@ -455,7 +455,6 @@ describe('CheckpointProposalJob Timing Tests', () => { worldState.fork.mockResolvedValue(mockFork); l1ToL2MessageSource = mock(); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(Array(4).fill(Fr.ZERO)); l1ToL2MessageSource.getInboxBucketByTotalMsgCount.mockResolvedValue({ seq: 0n, inboxRollingHash: Fr.ZERO, 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 5bb2f55958fa..036a23166e0f 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.ts @@ -648,10 +648,6 @@ export class CheckpointProposalJob implements Traceable { this.checkpointSimulationOverridesPlan, ); - // Under the streaming Inbox (AZIP-22 Fast Inbox) messages are selected per block, so the legacy inHash is fed - // zero; the running values are computed block by block. - const inHash = Fr.ZERO; - // Collect the out hashes of all the checkpoints before this one in the same epoch. // Under pipelining the parent checkpoint may not be on L1 yet at build time, so the helper // splices in the parent's checkpointOutHash from the locally-known proposed checkpoint so @@ -723,7 +719,6 @@ export class CheckpointProposalJob implements Traceable { const result = await this.buildBlocksForCheckpoint( checkpointBuilder, checkpointGlobalVariables.timestamp, - inHash, blockProposalOptions, streamingState, ); @@ -922,7 +917,6 @@ export class CheckpointProposalJob implements Traceable { private async buildBlocksForCheckpoint( checkpointBuilder: CheckpointBuilder, timestamp: bigint, - inHash: Fr, blockProposalOptions: BlockProposalOptions, streamingState?: StreamingCheckpointState, ): Promise<{ @@ -1020,9 +1014,7 @@ export class CheckpointProposalJob implements Traceable { usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString())); // Streaming Inbox: the block built successfully, so advance the consumption cursor and carry this block's - // rolling-hash bucket reference. A block that consumed nothing reuses the parent bucket reference. The legacy - // inHash is dead post-flip; block proposals carry zero (AZIP-22 Fast Inbox). - const blockInHash = inHash; + // rolling-hash bucket reference. A block that consumed nothing reuses the parent bucket reference. let blockBucketRef: InboxBucketRef | undefined = undefined; if (streamingState && selection) { if (selection.consume) { @@ -1035,7 +1027,6 @@ export class CheckpointProposalJob implements Traceable { // Sign the block proposal. This will throw if HA signing fails. const proposal = await this.createBlockProposal( block, - blockInHash, usedTxs, { ...blockProposalOptions, @@ -1084,7 +1075,6 @@ export class CheckpointProposalJob implements Traceable { /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */ private createBlockProposal( block: L2Block, - inHash: Fr, usedTxs: Tx[], blockProposalOptions: BlockProposalOptions, bucketRef?: InboxBucketRef, @@ -1097,7 +1087,6 @@ export class CheckpointProposalJob implements Traceable { block.header, this.checkpointNumber, block.indexWithinCheckpoint, - inHash, block.archive.root, usedTxs, this.proposer, @@ -1177,7 +1166,7 @@ export class CheckpointProposalJob implements Traceable { indexWithinCheckpoint: IndexWithinCheckpoint; buildDeadline: Date | undefined; txHashesAlreadyIncluded: Set; - /** Streaming Inbox message bundle to insert into this block's L1-to-L2 tree; undefined in the legacy flow. */ + /** Streaming Inbox message bundle for this block's L1-to-L2 tree; undefined when it consumes nothing. */ l1ToL2Messages?: Fr[]; }, ): Promise< diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 2f77bc1c6cc2..c3ca4b8b3f00 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -1,4 +1,3 @@ -import { MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants'; import { type EpochCache, type EpochCommitteeInfo, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache'; import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts'; import { @@ -349,7 +348,6 @@ describe('sequencer', () => { }); l1ToL2MessageSource = mock({ - getL1ToL2Messages: () => Promise.resolve(Array(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT).fill(Fr.ZERO)), getL2Tips: mockFn().mockResolvedValue({ proposed: { number: lastBlockNumber, hash }, checkpointed: { diff --git a/yarn-project/sequencer-client/src/test/utils.ts b/yarn-project/sequencer-client/src/test/utils.ts index 5bb506d4f729..484ef4a1e401 100644 --- a/yarn-project/sequencer-client/src/test/utils.ts +++ b/yarn-project/sequencer-client/src/test/utils.ts @@ -142,7 +142,6 @@ export function createBlockProposal(block: L2Block, signature: Signature): Block return new BlockProposal( block.header, block.indexWithinCheckpoint, - Fr.ZERO, // inHash - using zero for testing block.archive.root, txHashes, signature, diff --git a/yarn-project/stdlib/src/block/l2_block.ts b/yarn-project/stdlib/src/block/l2_block.ts index 06cd9133ac56..0d9975bee049 100644 --- a/yarn-project/stdlib/src/block/l2_block.ts +++ b/yarn-project/stdlib/src/block/l2_block.ts @@ -155,7 +155,6 @@ export class L2Block { * @param txsPerBlock - The number of transactions to include in the block. * @param numPublicCallsPerTx - The number of public function calls to include in each transaction. * @param numPublicLogsPerCall - The number of public logs per 1 public function invocation. - * @param inHash - The hash of the L1 to L2 messages subtree which got inserted in this block. * @returns The L2 block. */ static async random( diff --git a/yarn-project/stdlib/src/config/network-consensus-config.ts b/yarn-project/stdlib/src/config/network-consensus-config.ts index 152f29ed836c..c0fbeb13bb99 100644 --- a/yarn-project/stdlib/src/config/network-consensus-config.ts +++ b/yarn-project/stdlib/src/config/network-consensus-config.ts @@ -47,7 +47,6 @@ export const NETWORK_CONSENSUS_ENV_VARS = [ 'AZTEC_EJECTION_THRESHOLD', 'AZTEC_LOCAL_EJECTION_THRESHOLD', 'AZTEC_EXIT_DELAY_SECONDS', - 'AZTEC_INBOX_LAG', 'AZTEC_PROOF_SUBMISSION_EPOCHS', 'AZTEC_MANA_TARGET', 'AZTEC_PROVING_COST_PER_MANA', diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index c27cffd488c6..06ef09d352e3 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -204,11 +204,6 @@ describe('ArchiverApiSchema', () => { expect(result).toEqual([expect.any(Fr)]); }); - it('getL1ToL2Messages', async () => { - const result = await context.client.getL1ToL2Messages(CheckpointNumber(1)); - expect(result).toEqual([expect.any(Fr)]); - }); - it('getL1ToL2MessageIndex', async () => { const result = await context.client.getL1ToL2MessageIndex(Fr.random()); expect(result).toBe(1n); @@ -571,10 +566,6 @@ class MockArchiver implements ArchiverApi { expect(Array.isArray(signatures)).toBe(true); return Promise.resolve(); } - getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise { - expect(checkpointNumber).toEqual(CheckpointNumber(1)); - return Promise.resolve([Fr.random()]); - } getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { expect(l1ToL2Message).toBeInstanceOf(Fr); return Promise.resolve(1n); diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 3728c39090cc..a1a55af686fb 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -136,7 +136,6 @@ export const ArchiverApiSchema: ApiSchemaFor = { }), getContractClassIds: z.function({ input: z.tuple([]), output: z.array(schemas.Fr) }), registerContractFunctionSignatures: z.function({ input: z.tuple([z.array(z.string())]), output: z.void() }), - getL1ToL2Messages: z.function({ input: z.tuple([CheckpointNumberSchema]), output: z.array(schemas.Fr) }), getL1ToL2MessageIndex: z.function({ input: z.tuple([schemas.Fr]), output: schemas.BigInt.optional() }), getLatestInboxBucketAtOrBefore: z.function({ input: z.tuple([schemas.BigInt]), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index b0bfd03bf8eb..934ea3d0cde4 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -154,11 +154,6 @@ describe('AztecNodeApiSchema', () => { expect(response).toEqual([1n, expect.any(SiblingPath)]); }); - it('getL1ToL2MessageCheckpoint', async () => { - const response = await context.client.getL1ToL2MessageCheckpoint(Fr.random()); - expect(response).toEqual(5); - }); - it('getL1ToL2MessageIndex', async () => { const response = await context.client.getL1ToL2MessageIndex(Fr.random()); expect(response).toEqual(5n); @@ -715,10 +710,6 @@ class MockAztecNode implements AztecNode { expect(noteHash).toBeInstanceOf(Fr); return Promise.resolve(MembershipWitness.random(NOTE_HASH_TREE_HEIGHT)); } - getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { - expect(l1ToL2Message).toBeInstanceOf(Fr); - return Promise.resolve(CheckpointNumber(5)); - } getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise { expect(l1ToL2Message).toBeInstanceOf(Fr); return Promise.resolve(5n); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 26d9a2fb0556..6e5881f50ec7 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -200,13 +200,10 @@ export interface AztecNode { l1ToL2Message: Fr, ): Promise<[bigint, SiblingPath] | undefined>; - /** Returns the L2 checkpoint number in which this L1 to L2 message becomes available, or undefined if not found. */ - getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise; - /** * Returns the compact leaf index assigned to this L1 to L2 message as soon as the node has ingested it from L1, - * before any L2 block consumes it. Returns undefined if the node has not yet seen the message. Unlike - * {@link getL1ToL2MessageCheckpoint}, this does not require the L2 chain to have advanced past the message. + * before any L2 block consumes it. Returns undefined if the node has not yet seen the message. The message becomes + * consumable once a block's L1-to-L2 message tree grows past this index. */ getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise; @@ -616,8 +613,6 @@ export const AztecNodeApiSchema: ApiSchemaFor = { output: z.tuple([schemas.BigInt, SiblingPath.schemaFor(L1_TO_L2_MSG_TREE_HEIGHT)]).optional(), }), - getL1ToL2MessageCheckpoint: z.function({ input: z.tuple([schemas.Fr]), output: CheckpointNumberSchema.optional() }), - getL1ToL2MessageIndex: z.function({ input: z.tuple([schemas.Fr]), output: schemas.BigInt.optional() }), getL2ToL1Messages: z.function({ diff --git a/yarn-project/stdlib/src/interfaces/validator.ts b/yarn-project/stdlib/src/interfaces/validator.ts index f17e96e2a475..17903fdbe645 100644 --- a/yarn-project/stdlib/src/interfaces/validator.ts +++ b/yarn-project/stdlib/src/interfaces/validator.ts @@ -153,7 +153,6 @@ export interface Validator { blockHeader: BlockHeader, checkpointNumber: CheckpointNumber, indexWithinCheckpoint: number, - inHash: Fr, archive: Fr, txs: Tx[], proposerAddress: EthAddress | undefined, diff --git a/yarn-project/stdlib/src/messaging/in_hash.ts b/yarn-project/stdlib/src/messaging/in_hash.ts deleted file mode 100644 index fd5191abb5c1..000000000000 --- a/yarn-project/stdlib/src/messaging/in_hash.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants'; -import { padArrayEnd } from '@aztec/foundation/collection'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import { computeBalancedShaRoot } from '@aztec/foundation/trees'; - -/** - * Computes the inHash for a checkpoint (or the first block in a checkpoint) given its l1 to l2 messages. - */ -export function computeInHashFromL1ToL2Messages(unpaddedL1ToL2Messages: Fr[]): Fr { - const l1ToL2Messages = padArrayEnd(unpaddedL1ToL2Messages, Fr.ZERO, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT); - return new Fr(computeBalancedShaRoot(l1ToL2Messages.map(msg => msg.toBuffer()))); -} diff --git a/yarn-project/stdlib/src/messaging/inbox_leaf.ts b/yarn-project/stdlib/src/messaging/inbox_leaf.ts deleted file mode 100644 index 397c29a07efa..000000000000 --- a/yarn-project/stdlib/src/messaging/inbox_leaf.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { INITIAL_CHECKPOINT_NUMBER, MAX_L1_TO_L2_MSGS_PER_CHECKPOINT } from '@aztec/constants'; -import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; -import { CheckpointNumber } from '@aztec/foundation/branded-types'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; - -export class InboxLeaf { - constructor( - /** Index of the leaf in the whole tree. */ - public readonly index: bigint, - /** Leaf in the subtree/message hash. */ - public readonly leaf: Fr, - ) {} - - toBuffer(): Buffer { - return serializeToBuffer([this.index, this.leaf]); - } - - fromBuffer(buffer: Buffer | BufferReader): InboxLeaf { - const reader = BufferReader.asReader(buffer); - const index = toBigIntBE(reader.readBytes(32)); - const leaf = reader.readObject(Fr); - return new InboxLeaf(index, leaf); - } - - static smallestIndexForCheckpoint(checkpointNumber: CheckpointNumber): bigint { - return BigInt(checkpointNumber - INITIAL_CHECKPOINT_NUMBER) * BigInt(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT); - } - - /** - * Returns the range of valid indices for a given checkpoint. - * Start index is inclusive, end index is exclusive. - */ - static indexRangeForCheckpoint(checkpointNumber: CheckpointNumber): [bigint, bigint] { - const start = this.smallestIndexForCheckpoint(checkpointNumber); - const end = start + BigInt(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT); - return [start, end]; - } - - /** Returns the checkpoint number for a given leaf index */ - static checkpointNumberFromIndex(index: bigint): CheckpointNumber { - return CheckpointNumber(Number(index / BigInt(MAX_L1_TO_L2_MSGS_PER_CHECKPOINT)) + INITIAL_CHECKPOINT_NUMBER); - } -} diff --git a/yarn-project/stdlib/src/messaging/index.ts b/yarn-project/stdlib/src/messaging/index.ts index aad4f1a91990..3f8fc52654d4 100644 --- a/yarn-project/stdlib/src/messaging/index.ts +++ b/yarn-project/stdlib/src/messaging/index.ts @@ -1,8 +1,6 @@ export * from './append_l1_to_l2_messages.js'; -export * from './in_hash.js'; export * from './inbox_bucket.js'; export * from './inbox_consumption.js'; -export * from './inbox_leaf.js'; export * from './inbox_rolling_hash.js'; export * from './l1_to_l2_message_bundle.js'; export * from './l1_to_l2_message_sponge.js'; diff --git a/yarn-project/stdlib/src/messaging/l1_to_l2_message_source.ts b/yarn-project/stdlib/src/messaging/l1_to_l2_message_source.ts index 82610dbb461a..4040483f144b 100644 --- a/yarn-project/stdlib/src/messaging/l1_to_l2_message_source.ts +++ b/yarn-project/stdlib/src/messaging/l1_to_l2_message_source.ts @@ -1,4 +1,3 @@ -import type { CheckpointNumber } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { L2Tips } from '../block/l2_block_source.js'; @@ -8,15 +7,6 @@ import type { InboxBucket } from './inbox_bucket.js'; * Interface of classes allowing for the retrieval of L1 to L2 messages. */ export interface L1ToL2MessageSource { - /** - * Gets new L1 to L2 message (to be) included in a given checkpoint. - * @param checkpointNumber - Checkpoint number to get messages for. - * @returns The L1 to L2 messages/leaves of the messages subtree. - * @throws If the message tree for the given checkpoint has not yet been sealed on L1 - * (i.e., checkpointNumber >= inbox treeInProgress). - */ - getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise; - /** * Gets the L1 to L2 message index in the L1 to L2 message tree. * @param l1ToL2Message - The L1 to L2 message. diff --git a/yarn-project/stdlib/src/p2p/block_proposal.test.ts b/yarn-project/stdlib/src/p2p/block_proposal.test.ts index 1e37f9f001df..95ff697e5f64 100644 --- a/yarn-project/stdlib/src/p2p/block_proposal.test.ts +++ b/yarn-project/stdlib/src/p2p/block_proposal.test.ts @@ -23,7 +23,6 @@ const makeLegacyFixtureProposal = () => new BlockProposal( BlockHeader.empty(), IndexWithinCheckpoint(3), - new Fr(42n), new Fr(99n), [TxHash.fromField(new Fr(7n)), TxHash.fromField(new Fr(8n))], Signature.empty(), @@ -105,7 +104,6 @@ describe('Block Proposal serialization / deserialization', () => { const tampered = new BlockProposal( proposal.blockHeader, proposal.indexWithinCheckpoint, - proposal.inHash, proposal.archiveRoot, proposal.txHashes, proposal.signature, @@ -161,7 +159,6 @@ describe('Block Proposal serialization / deserialization', () => { const withoutRef = await makeBlockProposal({ blockHeader: withRef.blockHeader, indexWithinCheckpoint: withRef.indexWithinCheckpoint, - inHash: withRef.inHash, archiveRoot: withRef.archiveRoot, txHashes: withRef.txHashes, }); @@ -195,7 +192,6 @@ describe('Block Proposal serialization / deserialization', () => { const tampered = new BlockProposal( proposal.blockHeader, proposal.indexWithinCheckpoint, - proposal.inHash, proposal.archiveRoot, proposal.txHashes, proposal.signature, @@ -215,7 +211,6 @@ describe('Block Proposal serialization / deserialization', () => { const injected = new BlockProposal( proposal.blockHeader, proposal.indexWithinCheckpoint, - proposal.inHash, proposal.archiveRoot, proposal.txHashes, proposal.signature, diff --git a/yarn-project/stdlib/src/p2p/block_proposal.ts b/yarn-project/stdlib/src/p2p/block_proposal.ts index 750fde3f77ed..8bfe553c184f 100644 --- a/yarn-project/stdlib/src/p2p/block_proposal.ts +++ b/yarn-project/stdlib/src/p2p/block_proposal.ts @@ -71,9 +71,6 @@ export class BlockProposal extends Gossipable implements Signable { /** Index of this block within the checkpoint (0-indexed) */ public readonly indexWithinCheckpoint: IndexWithinCheckpoint, - /** Hash of L1 to L2 messages for this checkpoint (constant across all blocks in checkpoint) */ - public readonly inHash: Fr, - /** Archive root after this block is applied */ public readonly archiveRoot: Fr, @@ -132,15 +129,14 @@ export class BlockProposal extends Gossipable implements Signable { /** * Get the payload to sign for this block proposal. - * The signature is over: blockHeader + indexWithinCheckpoint + inHash + archiveRoot + txHashes, plus the bucket - * reference when set. Appending only when set keeps the pre-flip signed payload byte-identical to the legacy format, - * while binding the reference to the signature so a relay cannot strip or inject it without breaking recovery. + * The signature is over: blockHeader + indexWithinCheckpoint + archiveRoot + txHashes, plus the bucket reference + * when set. Appending only when set binds the reference to the signature so a relay cannot strip or inject it + * without breaking recovery. */ getPayloadToSign(): Buffer { return serializeToBuffer([ this.blockHeader, this.indexWithinCheckpoint, - this.inHash, this.archiveRoot, this.txHashes.length, this.txHashes, @@ -167,7 +163,6 @@ export class BlockProposal extends Gossipable implements Signable { blockHeader: BlockHeader, checkpointNumber: CheckpointNumber, indexWithinCheckpoint: IndexWithinCheckpoint, - inHash: Fr, archiveRoot: Fr, txHashes: TxHash[], txs: Tx[] | undefined, @@ -180,7 +175,6 @@ export class BlockProposal extends Gossipable implements Signable { const tempProposal = new BlockProposal( blockHeader, indexWithinCheckpoint, - inHash, archiveRoot, txHashes, Signature.empty(), @@ -216,7 +210,6 @@ export class BlockProposal extends Gossipable implements Signable { return new BlockProposal( blockHeader, indexWithinCheckpoint, - inHash, archiveRoot, txHashes, sig, @@ -263,7 +256,6 @@ export class BlockProposal extends Gossipable implements Signable { const buffer: any[] = [ this.blockHeader, this.indexWithinCheckpoint, - this.inHash, this.archiveRoot, this.signature, serializeCoordinationSignatureContext(this.signatureContext), @@ -276,8 +268,8 @@ export class BlockProposal extends Gossipable implements Signable { } else { buffer.push(0); // hasSignedTxs = false } - // Optional bucket-reference tail (AZIP-22 Fast Inbox). Appended only when set, so pre-flip proposals serialize - // byte-identically to the legacy format and mixed-version peers keep decoding them. + // Optional bucket-reference tail. Appended only when set, so a proposal without a reference + // serializes without the tail and a decoder that reaches EOF reads it as unset. if (this.bucketRef) { buffer.push(1); // hasBucketRef = true buffer.push(this.bucketRef.toBuffer()); @@ -290,7 +282,6 @@ export class BlockProposal extends Gossipable implements Signable { const blockHeader = reader.readObject(BlockHeader); const indexWithinCheckpoint = IndexWithinCheckpoint(reader.readNumber()); - const inHash = reader.readObject(Fr); const archiveRoot = reader.readObject(Fr); const signature = reader.readObject(Signature); const signatureContext = readCoordinationSignatureContext(reader); @@ -308,8 +299,8 @@ export class BlockProposal extends Gossipable implements Signable { } } - // Optional bucket-reference tail (AZIP-22 Fast Inbox). Legacy buffers end after the signedTxs flag, so EOF here - // decodes as "no reference" — this is the cross-version tolerance that keeps mixed-version gossip working. + // Optional bucket-reference tail. A buffer that ends after the signedTxs flag decodes as + // "no reference", so proposals written without the tail round-trip cleanly. let bucketRef: InboxBucketRef | undefined; if (!reader.isEmpty()) { const hasBucketRef = reader.readNumber(); @@ -321,7 +312,6 @@ export class BlockProposal extends Gossipable implements Signable { return new BlockProposal( blockHeader, indexWithinCheckpoint, - inHash, archiveRoot, txHashes, signature, @@ -335,7 +325,6 @@ export class BlockProposal extends Gossipable implements Signable { return ( this.blockHeader.getSize() + 4 /* indexWithinCheckpoint */ + - this.inHash.size + this.archiveRoot.size + this.signature.getSize() + 4 /* chainId */ + @@ -353,7 +342,6 @@ export class BlockProposal extends Gossipable implements Signable { BlockHeader.empty(), IndexWithinCheckpoint(0), Fr.ZERO, - Fr.ZERO, [], Signature.empty(), EMPTY_COORDINATION_SIGNATURE_CONTEXT, @@ -365,7 +353,6 @@ export class BlockProposal extends Gossipable implements Signable { BlockHeader.random(), IndexWithinCheckpoint(Math.floor(Math.random() * 5)), Fr.random(), - Fr.random(), [TxHash.random(), TxHash.random()], Signature.random(), EMPTY_COORDINATION_SIGNATURE_CONTEXT, @@ -376,7 +363,6 @@ export class BlockProposal extends Gossipable implements Signable { return { blockHeader: this.blockHeader.toInspect(), indexWithinCheckpoint: this.indexWithinCheckpoint, - inHash: this.inHash.toString(), archiveRoot: this.archiveRoot.toString(), signature: this.signature.toString(), txHashes: this.txHashes.map(h => h.toString()), @@ -404,7 +390,6 @@ export class BlockProposal extends Gossipable implements Signable { return new BlockProposal( this.blockHeader, this.indexWithinCheckpoint, - this.inHash, this.archiveRoot, this.txHashes, this.signature, diff --git a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts index 4c5f0f402927..a22dd24f21c2 100644 --- a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts +++ b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts @@ -110,8 +110,8 @@ export class CheckpointProposal extends Gossipable implements Signable { ) { super(); - // Check that last block properties match those of the checkpoint. The last block's bucket reference (AZIP-22 Fast - // Inbox) commits to the same rolling hash as the checkpoint header. Only enforced when the reference is set. + // Check that last block properties match those of the checkpoint. The last block's bucket reference + // commits to the same rolling hash as the checkpoint header. Only enforced when the reference is set. if (lastBlock?.bucketRef && !lastBlock.bucketRef.inboxRollingHash.equals(checkpointHeader.inboxRollingHash)) { throw new Error( `CheckpointProposal lastBlock bucketRef rolling hash ${lastBlock.bucketRef.inboxRollingHash} does not match checkpoint inboxRollingHash ${checkpointHeader.inboxRollingHash}`, @@ -150,7 +150,6 @@ export class CheckpointProposal extends Gossipable implements Signable { return new BlockProposal( this.lastBlock.blockHeader, this.lastBlock.indexWithinCheckpoint, - Fr.ZERO, this.archive, this.lastBlock.txHashes, this.lastBlock.signature, @@ -295,8 +294,8 @@ export class CheckpointProposal extends Gossipable implements Signable { } else { buffer.push(0); // hasSignedTxs = false } - // Optional bucket-reference tail (AZIP-22 Fast Inbox). Appended only when set, so pre-flip proposals serialize - // byte-identically to the legacy format and mixed-version peers keep decoding them. + // Optional bucket-reference tail. Appended only when set, so a proposal without a reference + // serializes without the tail and a decoder that reaches EOF reads it as unset. if (this.lastBlock.bucketRef) { buffer.push(1); // hasBucketRef = true buffer.push(this.lastBlock.bucketRef.toBuffer()); @@ -337,8 +336,8 @@ export class CheckpointProposal extends Gossipable implements Signable { } } - // Optional bucket-reference tail (AZIP-22 Fast Inbox). Legacy buffers end after the signedTxs flag, so EOF here - // decodes as "no reference" — the cross-version tolerance that keeps mixed-version gossip working. + // Optional bucket-reference tail. A buffer that ends after the signedTxs flag decodes as + // "no reference", so proposals written without the tail round-trip cleanly. let bucketRef: InboxBucketRef | undefined; if (!reader.isEmpty()) { const hasBucketRef = reader.readNumber(); diff --git a/yarn-project/stdlib/src/p2p/wire_compat_fixtures.ts b/yarn-project/stdlib/src/p2p/wire_compat_fixtures.ts index ca38bc7724f0..59813e0aee65 100644 --- a/yarn-project/stdlib/src/p2p/wire_compat_fixtures.ts +++ b/yarn-project/stdlib/src/p2p/wire_compat_fixtures.ts @@ -1,10 +1,10 @@ // Golden wire fixtures that pin the exact bytes a peer produces and consumes, so the optional bucket-reference tail // added to proposals stays wire compatible: an unset proposal must serialize to these bytes, and decoding these bytes -// must yield no bucket reference. The block-proposal fixtures predate the bucket-reference change (AZIP-22, A-1381); -// the checkpoint-proposal fixture was refreshed for the checkpoint-header format change that dropped `inHash`. +// must yield no bucket reference. The block-proposal fixtures were refreshed for the removal of the legacy `inHash` +// field; the checkpoint-proposal fixture was refreshed for the checkpoint-header format change. // // Both fixtures come from a deterministic proposal built with: -// BlockHeader.empty(), IndexWithinCheckpoint(3), inHash=Fr(42), archiveRoot=Fr(99), +// BlockHeader.empty(), IndexWithinCheckpoint(3), archiveRoot=Fr(99), // txHashes=[TxHash.fromField(Fr(7)), TxHash.fromField(Fr(8))], Signature.empty(), EMPTY_COORDINATION_SIGNATURE_CONTEXT // (checkpoint: CheckpointHeader.empty(), archive=Fr(123), feeAssetPriceModifier=0, empty signature/context, and a // lastBlock with BlockHeader.empty(), IndexWithinCheckpoint(4), txHashes=[TxHash.fromField(Fr(7))], empty signature). @@ -12,7 +12,7 @@ /** Legacy `BlockProposal.toBuffer()` bytes (no signedTxs, no bucket reference). */ export const LEGACY_BLOCK_PROPOSAL_HEX = - '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000800000000'; + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000800000000'; /** Legacy `CheckpointProposal.toBuffer()` bytes (lastBlock without signedTxs or bucket reference). */ export const LEGACY_CHECKPOINT_PROPOSAL_HEX = @@ -20,4 +20,4 @@ export const LEGACY_CHECKPOINT_PROPOSAL_HEX = /** Legacy `BlockProposal.getPayloadToSign()` bytes (no bucket reference). */ export const LEGACY_BLOCK_PROPOSAL_PAYLOAD_HEX = - '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000630000000200000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000008'; + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000630000000200000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000008'; diff --git a/yarn-project/stdlib/src/rollup/checkpoint_header.ts b/yarn-project/stdlib/src/rollup/checkpoint_header.ts index bd59f7746a97..85fece4ec6f2 100644 --- a/yarn-project/stdlib/src/rollup/checkpoint_header.ts +++ b/yarn-project/stdlib/src/rollup/checkpoint_header.ts @@ -31,8 +31,8 @@ export class CheckpointHeader { /** Hash of the blobs in the checkpoint. */ public blobsHash: Fr, /** - * Inbox rolling-hash chain value after consuming all L1-to-L2 messages bundled into this checkpoint (AZIP-22 Fast - * Inbox): the truncated-to-field sha256 chain the L1 Inbox accumulates. This is the checkpoint's only inbox + * Inbox rolling-hash chain value after consuming all L1-to-L2 messages bundled into this checkpoint: + * the truncated-to-field sha256 chain the L1 Inbox accumulates. This is the checkpoint's only inbox * commitment. */ public inboxRollingHash: Fr, diff --git a/yarn-project/stdlib/src/rollup/checkpoint_root_rollup_private_inputs.ts b/yarn-project/stdlib/src/rollup/checkpoint_root_rollup_private_inputs.ts index 01f908d77221..73230264205f 100644 --- a/yarn-project/stdlib/src/rollup/checkpoint_root_rollup_private_inputs.ts +++ b/yarn-project/stdlib/src/rollup/checkpoint_root_rollup_private_inputs.ts @@ -119,7 +119,7 @@ export class CheckpointRootRollupPrivateInputs { RollupHonkProofData, ], /** - * Inbox parity proof over the checkpoint's L1-to-L2 messages (AZIP-22 Fast Inbox): it commits to the checkpoint's + * Inbox parity proof over the checkpoint's L1-to-L2 messages: it commits to the checkpoint's * message list, and its message sponge is checked against the blocks' accumulated one. */ public inboxParity: UltraHonkProofData, @@ -168,7 +168,7 @@ export class CheckpointRootSingleBlockRollupPrivateInputs { constructor( public previousRollup: RollupHonkProofData, /** - * Inbox parity proof over the checkpoint's L1-to-L2 messages (AZIP-22 Fast Inbox): it commits to the checkpoint's + * Inbox parity proof over the checkpoint's L1-to-L2 messages: it commits to the checkpoint's * message list, and its message sponge is checked against the block's accumulated one. */ public inboxParity: UltraHonkProofData, diff --git a/yarn-project/stdlib/src/tests/mocks.ts b/yarn-project/stdlib/src/tests/mocks.ts index 8c3f6a990aa0..1b6a05fc95d2 100644 --- a/yarn-project/stdlib/src/tests/mocks.ts +++ b/yarn-project/stdlib/src/tests/mocks.ts @@ -542,7 +542,6 @@ export interface MakeBlockProposalOptions { signer?: Secp256k1Signer; blockHeader?: BlockHeader; indexWithinCheckpoint?: IndexWithinCheckpoint; - inHash?: Fr; archiveRoot?: Fr; txHashes?: TxHash[]; txs?: Tx[]; @@ -595,7 +594,6 @@ export const makeAndSignCommitteeAttestationsAndSigners = ( export const makeBlockProposal = (options?: MakeBlockProposalOptions): Promise => { const blockHeader = options?.blockHeader ?? makeBlockHeader(1); const indexWithinCheckpoint = options?.indexWithinCheckpoint ?? IndexWithinCheckpoint(0); - const inHash = options?.inHash ?? Fr.random(); const archiveRoot = options?.archiveRoot ?? Fr.random(); const txHashes = options?.txHashes ?? [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); const txs = options?.txs; @@ -607,7 +605,6 @@ export const makeBlockProposal = (options?: MakeBlockProposalOptions): Promise 0, then validate global variables match parent (chainId, version, slotNumber, timestamp, coinbase, feeRecipient, gasFees) -7. Verify inHash matches computed from L1-to-L2 messages +7. Run the streaming Inbox acceptance checks on the proposal's bucket reference and derive the block's L1-to-L2 message bundle (AZIP-22 Fast Inbox) 8. Collect transactions from pool/network/proposal 9. Re-execute transactions (if enabled) 10. Compare re-execution result with proposal diff --git a/yarn-project/validator-client/src/duties/validation_service.test.ts b/yarn-project/validator-client/src/duties/validation_service.test.ts index d78c67628e4d..b631709247e5 100644 --- a/yarn-project/validator-client/src/duties/validation_service.test.ts +++ b/yarn-project/validator-client/src/duties/validation_service.test.ts @@ -34,14 +34,12 @@ describe('ValidationService', () => { const txs = await Promise.all([Tx.random(), Tx.random()]); const blockHeader = makeBlockHeader(1); const indexWithinCheckpoint = IndexWithinCheckpoint(0); - const inHash = Fr.random(); const archive = Fr.random(); const proposal = await service.createBlockProposal( blockHeader, CheckpointNumber(1), indexWithinCheckpoint, - inHash, archive, txs, addresses[0], @@ -56,14 +54,12 @@ describe('ValidationService', () => { const txs = await Promise.all([Tx.random(), Tx.random()]); const blockHeader = makeBlockHeader(1); const indexWithinCheckpoint = IndexWithinCheckpoint(0); - const inHash = Fr.random(); const archive = Fr.random(); const proposal = await service.createBlockProposal( blockHeader, CheckpointNumber(1), indexWithinCheckpoint, - inHash, archive, txs, addresses[0], @@ -90,12 +86,11 @@ describe('ValidationService', () => { const checkpointHeader = makeCheckpointHeader(1); // Create the block proposal first (as the sequencer would) so that getSender() can verify the block proposal - // sender matches. The block-level inHash is dead post-flip (AZIP-22 Fast Inbox), so it carries zero. + // sender matches. const blockProposal = await service.createBlockProposal( blockHeader, CheckpointNumber(1), indexWithinCheckpoint, - Fr.ZERO, archive, txs, addresses[0], diff --git a/yarn-project/validator-client/src/duties/validation_service.ts b/yarn-project/validator-client/src/duties/validation_service.ts index d5f2157d7a49..c6c7d1143def 100644 --- a/yarn-project/validator-client/src/duties/validation_service.ts +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -35,7 +35,6 @@ export class ValidationService { * * @param blockHeader - The block header * @param blockIndexWithinCheckpoint - The block index within checkpoint for HA signing context - * @param inHash - Hash of L1 to L2 messages for this checkpoint * @param archive - The archive of the current block * @param txs - Ordered list of transactions (Tx[]) * @param proposerAttesterAddress - The address of the proposer/attester, or undefined @@ -49,7 +48,6 @@ export class ValidationService { blockHeader: BlockHeader, checkpointNumber: CheckpointNumber, blockIndexWithinCheckpoint: IndexWithinCheckpoint, - inHash: Fr, archive: Fr, txs: Tx[], proposerAttesterAddress: EthAddress | undefined, @@ -77,7 +75,6 @@ export class ValidationService { blockHeader, checkpointNumber, blockIndexWithinCheckpoint, - inHash, archive, txs.map(tx => tx.getTxHash()), options.publishFullTxs ? txs : undefined, diff --git a/yarn-project/validator-client/src/metrics.ts b/yarn-project/validator-client/src/metrics.ts index 8aad926ca859..401584c6a5f7 100644 --- a/yarn-project/validator-client/src/metrics.ts +++ b/yarn-project/validator-client/src/metrics.ts @@ -43,13 +43,7 @@ export class ValidatorMetrics { meter, Metrics.VALIDATOR_ATTESTATION_FAILED_BAD_PROPOSAL_COUNT, { - [Attributes.ERROR_TYPE]: [ - 'invalid_proposal', - 'state_mismatch', - 'failed_txs', - 'in_hash_mismatch', - 'parent_block_wrong_slot', - ], + [Attributes.ERROR_TYPE]: ['invalid_proposal', 'state_mismatch', 'failed_txs', 'parent_block_wrong_slot'], [Attributes.IS_COMMITTEE_MEMBER]: [true, false], }, ); diff --git a/yarn-project/validator-client/src/proposal_handler.test.ts b/yarn-project/validator-client/src/proposal_handler.test.ts index 43c6abf33888..fadffd8fb907 100644 --- a/yarn-project/validator-client/src/proposal_handler.test.ts +++ b/yarn-project/validator-client/src/proposal_handler.test.ts @@ -66,7 +66,6 @@ describe('ProposalHandler checkpoint validation', () => { blockSource.syncImmediate.mockResolvedValue(undefined); l1ToL2MessageSource = mock(); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); checkpointsBuilder = mock(); checkpointsBuilder.getConfig.mockReturnValue({ diff --git a/yarn-project/validator-client/src/proposal_handler.ts b/yarn-project/validator-client/src/proposal_handler.ts index 345dd23208a5..fe0037e5ba8f 100644 --- a/yarn-project/validator-client/src/proposal_handler.ts +++ b/yarn-project/validator-client/src/proposal_handler.ts @@ -72,7 +72,6 @@ export type BlockProposalValidationFailureReason = | 'invalid_proposal' | 'parent_block_not_found' | 'parent_block_wrong_slot' - | 'in_hash_mismatch' // Streaming Inbox per-block acceptance failures. | StreamingBlockCheckReason | 'global_variables_mismatch' @@ -191,7 +190,6 @@ export const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidation 'global_variables_mismatch', 'invalid_proposal', 'parent_block_wrong_slot', - 'in_hash_mismatch', ]; /** Checkpoint-proposal validation failures that constitute a slashable invalid-checkpoint offense. */ 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 b6bd10e4ad1d..e1f9e292ca7b 100644 --- a/yarn-project/validator-client/src/validator.ha.integration.test.ts +++ b/yarn-project/validator-client/src/validator.ha.integration.test.ts @@ -19,7 +19,6 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2BlockSink, L2BlockSource } from '@aztec/stdlib/block'; import { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint'; import type { SlasherConfig, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; -import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { TEST_COORDINATION_SIGNATURE_CONTEXT, @@ -115,7 +114,6 @@ describe('ValidatorClient HA Integration', () => { blockSource = mock(); l1ToL2MessageSource = mock(); txProvider = mock(); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); dateProvider = new TestDateProvider(); blobClient = mock(); blobClient.canUpload.mockReturnValue(false); @@ -306,7 +304,6 @@ describe('ValidatorClient HA Integration', () => { // Use all 5 validators - all try to create the same block proposal const blockHeader = makeBlockHeader(1); const indexWithinCheckpoint = IndexWithinCheckpoint(0); - const inHash = computeInHashFromL1ToL2Messages([]); const archive = Fr.random(); const txs = await Promise.all([1, 2, 3].map(() => mockTx())); const proposerAddress = EthAddress.fromString(validatorAccounts[0].address); @@ -318,7 +315,6 @@ describe('ValidatorClient HA Integration', () => { blockHeader, CheckpointNumber(1), indexWithinCheckpoint, - inHash, archive, txs, proposerAddress, @@ -348,7 +344,6 @@ describe('ValidatorClient HA Integration', () => { it('should allow different validators to create proposals for different slots', async () => { const proposerAddress = EthAddress.fromString(validatorAccounts[0].address); const txs = await Promise.all([1, 2, 3].map(() => mockTx())); - const inHash = computeInHashFromL1ToL2Messages([]); // Each of the 5 validators creates a proposal for a different slot const proposals = await Promise.all( @@ -359,7 +354,6 @@ describe('ValidatorClient HA Integration', () => { blockHeader, CheckpointNumber(1), IndexWithinCheckpoint(0), - inHash, archive, txs, proposerAddress, diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index d12bb5608f50..ee3c996e3b76 100644 --- a/yarn-project/validator-client/src/validator.integration.test.ts +++ b/yarn-project/validator-client/src/validator.integration.test.ts @@ -26,7 +26,7 @@ import { CheckpointReexecutionTracker, L1PublishedData, PublishedCheckpoint } fr import { type L1RollupConstants, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import { Gas, GasFees } from '@aztec/stdlib/gas'; import { tryStop } from '@aztec/stdlib/interfaces/server'; -import { InboxBucketRef, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging'; +import { InboxBucketRef } from '@aztec/stdlib/messaging'; import { type BlockProposal, CheckpointProposal } from '@aztec/stdlib/p2p'; import { mockTx } from '@aztec/stdlib/testing'; import { BlockHeader, type CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx'; @@ -240,7 +240,7 @@ describe('ValidatorClient Integration', () => { type BlockProposalResult = { block: L2Block; proposal: BlockProposal }; - /** Builds a new block proposal with the given txs and l1-to-l2 messages */ + /** Builds a new block proposal with the given txs and L1-to-L2 message bundle */ const buildBlockProposal = async ( checkpointBuilder: CheckpointBuilder, blockNumber: BlockNumber, @@ -248,7 +248,6 @@ describe('ValidatorClient Integration', () => { txs: Tx[] = [], l1ToL2Messages: Fr[] = [], ): Promise<{ block: L2Block; proposal: BlockProposal }> => { - const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages); const blockTimestamp = getTimestampForSlot(checkpointBuilder.getConstantData().slotNumber, l1Constants); const { block, usedTxs } = await checkpointBuilder.buildBlock(txs, blockNumber, blockTimestamp, { isBuildingProposal: true, @@ -270,7 +269,6 @@ describe('ValidatorClient Integration', () => { block.header, cpNumber, block.indexWithinCheckpoint, - inHash, block.archive.root, usedTxs, proposerSigner.address, @@ -478,7 +476,7 @@ describe('ValidatorClient Integration', () => { it('validates and attests with txs anchored to proposed blocks and non-empty l1-to-l2 messages', async () => { // Create l1 to l2 messages and seed them into the archivers - const l1ToL2Messages = makeInboxMessages(4, { messagesPerCheckpoint: 4 }); + const l1ToL2Messages = makeInboxMessages(4); await proposer.archiver.dataStores.messages.addL1ToL2MessageBuckets(l1ToL2Messages); await attestor.archiver.dataStores.messages.addL1ToL2MessageBuckets(l1ToL2Messages); @@ -678,10 +676,10 @@ describe('ValidatorClient Integration', () => { }); it('refuses block proposal with mismatching l1 to l2 messages', async () => { - const l1ToL2Messages = makeInboxMessages(4, { messagesPerCheckpoint: 4 }); + const l1ToL2Messages = makeInboxMessages(4); await proposer.archiver.dataStores.messages.addL1ToL2MessageBuckets(l1ToL2Messages); - const otherL1ToL2Messages = makeInboxMessages(4, { messagesPerCheckpoint: 4 }); + const otherL1ToL2Messages = makeInboxMessages(4); await attestor.archiver.dataStores.messages.addL1ToL2MessageBuckets(otherL1ToL2Messages); const { blocks } = await buildCheckpoint( diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index 0a0ae849ea4a..47f4ef07cd46 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -32,12 +32,7 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type BlockData, BlockHash, L2Block, type L2BlockSink, type L2BlockSource } from '@aztec/stdlib/block'; import { type Checkpoint, CheckpointReexecutionTracker, type ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { SlasherConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; -import { - type InboxBucket, - InboxBucketRef, - type L1ToL2MessageSource, - computeInHashFromL1ToL2Messages, -} from '@aztec/stdlib/messaging'; +import { type InboxBucket, InboxBucketRef, type L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import type { BlockProposal } from '@aztec/stdlib/p2p'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { @@ -184,7 +179,6 @@ describe('ValidatorClient', () => { epochCache.isEscapeHatchOpenAtSlot.mockResolvedValue(false); l1ToL2MessageSource = mock(); txProvider = mock(); - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); dateProvider = new TestDateProvider(); blobClient = mock(); blobClient.canUpload.mockReturnValue(false); @@ -243,7 +237,6 @@ describe('ValidatorClient', () => { it('should create a valid block proposal without txs', async () => { const blockHeader = makeBlockHeader(); const indexWithinCheckpoint = IndexWithinCheckpoint(0); - const inHash = Fr.random(); const archive = Fr.random(); const txs = await Promise.all([1, 2, 3, 4, 5].map(() => mockTx())); @@ -251,7 +244,6 @@ describe('ValidatorClient', () => { blockHeader, CheckpointNumber(1), indexWithinCheckpoint, - inHash, archive, txs, EthAddress.fromString(validatorAccounts[0].address), @@ -471,10 +463,9 @@ describe('ValidatorClient', () => { const genesisBucketRef = InboxBucketRef.fromBucket(genesisInboxBucket); beforeEach(async () => { - const emptyInHash = computeInHashFromL1ToL2Messages([]); const blockHeader = makeBlockHeader(1, { blockNumber: BlockNumber(100), slotNumber: SlotNumber(100) }); blockNumber = BlockNumber(blockHeader.globalVariables.blockNumber); - proposal = await makeBlockProposal({ blockHeader, inHash: emptyInHash, bucketRef: genesisBucketRef }); + proposal = await makeBlockProposal({ blockHeader, bucketRef: genesisBucketRef }); // The proposal targets slot 100, which under pipelining is built during the previous slot. Set the // wall clock to the start of that build slot (target_slot_start - S), matching how a pipelined // proposer is positioned when validating an inbound block proposal. With S - 2E = 0 in this config @@ -590,7 +581,6 @@ describe('ValidatorClient', () => { validatorClient.getProposalHandler().register(p2pClient, true); const signer = Secp256k1Signer.random(); - const emptyInHash = computeInHashFromL1ToL2Messages([]); const checkpointProposal = await makeCheckpointProposal({ signer, checkpointHeader: makeCheckpointHeader(1, { slotNumber: proposal.slotNumber }), @@ -619,7 +609,6 @@ describe('ValidatorClient', () => { signer, blockHeader: laterBlockHeader, indexWithinCheckpoint: IndexWithinCheckpoint(1), - inHash: emptyInHash, archiveRoot: Fr.random(), }); @@ -708,7 +697,6 @@ describe('ValidatorClient', () => { blockNumber, slotNumber: futureSlot, }), - inHash: computeInHashFromL1ToL2Messages([]), }); // Under pipelining, the target slot is the future slot the proposer is building for, built during @@ -747,10 +735,8 @@ describe('ValidatorClient', () => { it('should process block proposal from own validator key (HA peer)', async () => { const selfSigner = new Secp256k1Signer(Buffer32.fromString(validatorPrivateKeys[0])); - const emptyInHash = computeInHashFromL1ToL2Messages([]); const selfProposal = await makeBlockProposal({ blockHeader: proposal.blockHeader, - inHash: emptyInHash, archiveRoot: proposal.archive, txHashes: proposal.txHashes, signer: selfSigner, @@ -1460,10 +1446,6 @@ describe('ValidatorClient', () => { describe('non-first block in checkpoint validation', () => { // When indexWithinCheckpoint > 0, global variables must match parent block (except blockNumber). - // The inHash validation is implicitly handled: all blocks in a checkpoint share the same - // checkpointNumber, so they fetch the same L1-to-L2 messages and compute the same inHash. - // If a proposal has a different inHash, the existing validation (which computes inHash from - // L1 messages for the checkpoint) will catch it. it('should return false if global variables do not match parent for non-first block in checkpoint', async () => { // Create a proposal with indexWithinCheckpoint > 0 (non-first block in checkpoint) @@ -1485,8 +1467,6 @@ describe('ValidatorClient', () => { coinbase: EthAddress.random(), // Different from parent - should cause failure }); - // Use empty messages and compute the matching inHash - const emptyInHash = computeInHashFromL1ToL2Messages([]); const proposalBlockHeader = makeBlockHeader(1, { blockNumber: BlockNumber(parentBlockNumber + 1), slotNumber: SlotNumber(parentSlotNumber), @@ -1497,7 +1477,6 @@ describe('ValidatorClient', () => { const nonFirstBlockProposal = await makeBlockProposal({ blockHeader: proposalBlockHeader, indexWithinCheckpoint: IndexWithinCheckpoint(1), // Non-first block in checkpoint - inHash: emptyInHash, }); // Update epochCache mock for the new proposal @@ -1558,13 +1537,6 @@ describe('ValidatorClient', () => { const isValid = await validatorClient.validateBlockProposal(nonFirstBlockProposal, sender); expect(isValid).toBe(false); }); - - // Note: inHash validation for non-first blocks is implicitly handled by the existing - // validation that computes inHash from L1-to-L2 messages for the checkpoint. Since all - // blocks in the same checkpoint share the same checkpointNumber, they will always - // compute the same inHash from the same L1 messages. If a malicious proposal has a - // different inHash, it will fail the existing validation at lines 192-200 in - // proposal_handler.ts. }); it('should validate proposals in fisherman mode but not create or broadcast attestations', async () => { diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 97f8e6f9114e..b3f93748de1f 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -478,7 +478,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) 'invalid_proposal', 'state_mismatch', 'failed_txs', - 'in_hash_mismatch', 'parent_block_wrong_slot', ]; @@ -917,7 +916,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) blockHeader: BlockHeader, checkpointNumber: CheckpointNumber, indexWithinCheckpoint: IndexWithinCheckpoint, - inHash: Fr, archive: Fr, txs: Tx[], proposerAddress: EthAddress | undefined, @@ -945,7 +943,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) blockHeader, checkpointNumber, indexWithinCheckpoint, - inHash, archive, txs, proposerAddress, diff --git a/yarn-project/world-state/src/native/native_world_state.ts b/yarn-project/world-state/src/native/native_world_state.ts index 432c70e566b2..2adde41e0bce 100644 --- a/yarn-project/world-state/src/native/native_world_state.ts +++ b/yarn-project/world-state/src/native/native_world_state.ts @@ -266,7 +266,6 @@ export class NativeWorldStateService implements MerkleTreeDatabase { public async handleL2BlockAndMessages(l2Block: L2Block, l1ToL2Messages: Fr[]): Promise { // Any block may carry an L1-to-L2 message bundle and transition the L1-to-L2 message tree by its real (unpadded, // compact) leaves, matching how the circuits build the tree. - const paddedL1ToL2Messages = l1ToL2Messages; // We have to pad the note hashes and nullifiers within tx effects because that's how the trees are built by // circuits. @@ -294,7 +293,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { // Forwarded so the native sync verifies the archive root against canonical and rejects a divergent tree. expectedArchiveRoot: l2Block.archive.root.toBuffer(), expectedPreviousArchiveRoot: l2Block.header.lastArchive.root.toBuffer(), - paddedL1ToL2Messages: paddedL1ToL2Messages.map(serializeLeaf), + paddedL1ToL2Messages: l1ToL2Messages.map(serializeLeaf), paddedNoteHashes: paddedNoteHashes.map(serializeLeaf), paddedNullifiers: paddedNullifiers.map(serializeLeaf), publicDataWrites: publicDataWrites.map(serializeLeaf), diff --git a/yarn-project/world-state/src/world-state-db/merkle_tree_db.ts b/yarn-project/world-state/src/world-state-db/merkle_tree_db.ts index f1cb5cb06f70..6dd5f77baa9c 100644 --- a/yarn-project/world-state/src/world-state-db/merkle_tree_db.ts +++ b/yarn-project/world-state/src/world-state-db/merkle_tree_db.ts @@ -33,9 +33,8 @@ export interface MerkleTreeAdminDatabase extends ForkMerkleTreeOperations, Reado /** * Handles a single L2 block: inserts its note hashes, nullifiers, public data writes, and the block's L1-to-L2 * message bundle into the merkle trees. Any block may carry a message bundle and transition the L1-to-L2 message - * tree, not just the first block of a checkpoint. A first-in-checkpoint bundle is padded to - * MAX_L1_TO_L2_MSGS_PER_CHECKPOINT to match how the circuits build the tree; a non-first bundle is appended - * exactly as given. Padding is a transitional concern of this method that moves entirely to the caller at the flip. + * tree, not just the first block of a checkpoint. The bundle's real (unpadded, compact) leaves are appended as + * given, matching how the circuits build the tree. * @param block - The L2 block to handle. * @param l1ToL2Messages - The L1 to L2 messages for the block. */