From a79cd8d841524e299b4f0c22d8dd903d82099fc6 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 25 Feb 2026 21:11:05 +0000 Subject: [PATCH 1/9] Simulate at correct block --- .../aztec-node/src/aztec-node/server.test.ts | 99 +++++++++++++++++-- .../aztec-node/src/aztec-node/server.ts | 29 +++++- yarn-project/bot/src/cross_chain_bot.ts | 7 +- yarn-project/bot/src/factory.ts | 10 +- .../l1_to_l2.test.ts | 51 ++++++++++ 5 files changed, 175 insertions(+), 21 deletions(-) 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 17d6f3f51928..9e62e110b2cf 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1,7 +1,9 @@ import { TestCircuitVerifier } from '@aztec/bb-prover'; +import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import { EpochCache } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; +import { padArrayEnd } 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'; @@ -16,14 +18,24 @@ import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-ju import type { GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client'; import type { SlasherClientInterface } from '@aztec/slasher'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { L2Block, type L2BlockSource } from '@aztec/stdlib/block'; +import { type BlockData, L2Block, type L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; -import type { L2LogsSource, MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; +import type { + L2LogsSource, + MerkleTreeReadOperations, + MerkleTreeWriteOperations, + WorldStateSynchronizer, +} from '@aztec/stdlib/interfaces/server'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { mockTx } from '@aztec/stdlib/testing'; -import { MerkleTreeId, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees'; +import { + AppendOnlyTreeSnapshot, + MerkleTreeId, + PublicDataTreeLeaf, + PublicDataTreeLeafPreimage, +} from '@aztec/stdlib/trees'; import { BlockHeader, GlobalVariables, @@ -67,6 +79,8 @@ describe('aztec node', () => { let globalVariablesBuilder: MockProxy; let merkleTreeOps: MockProxy; let l2BlockSource: MockProxy; + let l1ToL2MessageSource: MockProxy; + let worldState: MockProxy; let lastBlockNumber: BlockNumber; let node: AztecNodeService; let feePayer: AztecAddress; @@ -130,7 +144,7 @@ describe('aztec node', () => { } }); - const worldState = mock({ + worldState = mock({ getCommitted: () => merkleTreeOps, }); @@ -139,7 +153,7 @@ describe('aztec node', () => { const l2LogsSource = mock(); - const l1ToL2MessageSource = mock(); + l1ToL2MessageSource = mock(); // all txs use the same allowed FPC class const contractSource = mock(); @@ -398,6 +412,79 @@ describe('aztec node', () => { unfreeze(tx.data.constants.txContext.gasSettings.gasLimits).l2Gas = 1e12; await expect(node.simulatePublicCalls(tx)).rejects.toThrow(/gas/i); }); + + it('appends pending L1-to-L2 messages to the simulation fork', async () => { + lastBlockNumber = BlockNumber(5); + + // Mock buildGlobalVariables so the method doesn't crash before reaching the fork + globalVariablesBuilder.buildGlobalVariables.mockResolvedValue( + GlobalVariables.empty({ blockNumber: BlockNumber(6) }), + ); + + // Mock getBlockData to return block data with a checkpoint number + const blockData: BlockData = { + header: BlockHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + blockHash: Fr.ZERO, + checkpointNumber: CheckpointNumber(5), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + }; + l2BlockSource.getBlockData.mockResolvedValue(blockData); + + // Mock L1-to-L2 messages for the next checkpoint + const pendingMessages = [Fr.random(), Fr.random(), Fr.random()]; + l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(pendingMessages); + + // Mock the fork to track appendLeaves calls + const merkleTreeFork = mock(); + worldState.fork.mockResolvedValue(merkleTreeFork); + + // simulatePublicCalls will fail during actual processing since we don't set up a + // full processor, but we only care about verifying that appendLeaves was called + const tx = await mockTxForRollup(0x10000); + await node.simulatePublicCalls(tx).catch(() => {}); + + // Verify that appendLeaves was called with the pending messages padded to the correct size + expect(merkleTreeFork.appendLeaves).toHaveBeenCalledWith( + MerkleTreeId.L1_TO_L2_MESSAGE_TREE, + padArrayEnd(pendingMessages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), + ); + + // Verify the messages were fetched for the NEXT checkpoint (current + 1) + expect(l1ToL2MessageSource.getL1ToL2Messages).toHaveBeenCalledWith(CheckpointNumber(6)); + + // Verify fork was closed + expect(merkleTreeFork.close).toHaveBeenCalled(); + }); + + it('skips appending when there are no pending L1-to-L2 messages', async () => { + lastBlockNumber = BlockNumber(3); + + globalVariablesBuilder.buildGlobalVariables.mockResolvedValue( + GlobalVariables.empty({ blockNumber: BlockNumber(4) }), + ); + + const blockData: BlockData = { + header: BlockHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + blockHash: Fr.ZERO, + checkpointNumber: CheckpointNumber(3), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + }; + l2BlockSource.getBlockData.mockResolvedValue(blockData); + + // No pending messages for the next checkpoint + l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); + + const merkleTreeFork = mock(); + worldState.fork.mockResolvedValue(merkleTreeFork); + + const tx = await mockTxForRollup(0x10000); + await node.simulatePublicCalls(tx).catch(() => {}); + + // appendLeaves should NOT be called when there are no messages + expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled(); + }); }); describe('reloadKeystore', () => { diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 5b7f3f74b19f..57803f657008 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -2,14 +2,19 @@ import { Archiver, createArchiver } from '@aztec/archiver'; import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client'; import { Blob } from '@aztec/blob-lib'; -import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; +import { + ARCHIVE_HEIGHT, + type L1_TO_L2_MSG_TREE_HEIGHT, + type NOTE_HASH_TREE_HEIGHT, + NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, +} from '@aztec/constants'; import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { getPublicClient } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { compactArray, pick, unique } from '@aztec/foundation/collection'; +import { compactArray, padArrayEnd, pick, unique } 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'; @@ -1254,6 +1259,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { await this.worldStateSynchronizer.syncImmediate(latestBlockNumber); const merkleTreeFork = await this.worldStateSynchronizer.fork(); try { + // Append pending L1-to-L2 messages for the next checkpoint to the simulation fork. + // This mirrors what the sequencer does in LightweightCheckpointBuilder.startNewCheckpoint(): + // it appends messages to the L1_TO_L2_MESSAGE_TREE BEFORE executing block TXs. Without this, + // simulations that consume L1-to-L2 messages fail because the messages aren't in the tree yet. + // See https://linear.app/aztec-labs/issue/A-548 for details. + const blockData = await this.blockSource.getBlockData(latestBlockNumber); + if (blockData) { + const nextCheckpointNumber = CheckpointNumber(blockData.checkpointNumber + 1); + const pendingL1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(nextCheckpointNumber); + if (pendingL1ToL2Messages.length > 0) { + await merkleTreeFork.appendLeaves( + MerkleTreeId.L1_TO_L2_MESSAGE_TREE, + padArrayEnd(pendingL1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), + ); + this.log.verbose( + `Appended ${pendingL1ToL2Messages.length} pending L1-to-L2 messages for checkpoint ${nextCheckpointNumber} to simulation fork`, + ); + } + } + const config = PublicSimulatorConfig.from({ skipFeeEnforcement, collectDebugLogs: true, diff --git a/yarn-project/bot/src/cross_chain_bot.ts b/yarn-project/bot/src/cross_chain_bot.ts index 0165b5a778a8..df89ba5e6afb 100644 --- a/yarn-project/bot/src/cross_chain_bot.ts +++ b/yarn-project/bot/src/cross_chain_bot.ts @@ -175,12 +175,7 @@ export class CrossChainBot extends BaseBot { const now = Date.now(); for (const msg of pendingMessages) { const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash), { - // Use forPublicConsumption: false so we wait until blockNumber >= messageBlockNumber. - // With forPublicConsumption: true, the check returns true one block early (the sequencer - // includes L1→L2 messages before executing the block's txs), but gas estimation simulates - // against the current world state which doesn't yet have the message. - // See https://linear.app/aztec-labs/issue/A-548 for details. - forPublicConsumption: false, + forPublicConsumption: true, }); if (ready) { return msg; diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 31b05f540b8e..d84beb632b5c 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -158,11 +158,7 @@ export class BotFactory { const firstMsg = allMessages[0]; await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - // Use forPublicConsumption: false so we wait until the message is in the current world - // state. With true, it returns one block early which causes gas estimation simulation to - // fail since it runs against the current state. - // See https://linear.app/aztec-labs/issue/A-548 for details. - forPublicConsumption: false, + forPublicConsumption: true, }); this.log.info(`First L1→L2 message is ready`); } @@ -507,7 +503,7 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, messageHash, { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: false, + forPublicConsumption: true, }), ); return existingClaim.claim; @@ -546,7 +542,7 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: false, + forPublicConsumption: true, }), ); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index e83b195c5f84..6cb7b619ec8a 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -162,6 +162,57 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { 120_000, ); + // Regression test for A-548: when isL1ToL2MessageReady returns true with forPublicConsumption: true, + // the simulator should be able to simulate a tx that consumes the message. Before the fix, + // simulation would revert because the public VM ran against the current world state which didn't + // yet contain the message (it would only be added by the sequencer when building the next block). + it('can simulate public consumption of L1 to L2 message when forPublicConsumption is true', async () => { + // Send L1 to L2 message + const [secret, secretHash] = await generateClaimSecret(); + const message = { recipient: testContract.address, content: Fr.random(), secretHash }; + const { msgHash, globalLeafIndex } = await sendL1ToL2Message(message, crossChainTestHarness); + + // Wait for the archiver to sync the message and get the target block number + const msgBlockNumber = await waitForMessageFetched(msgHash); + log.warn(`Message fetched, target block: ${msgBlockNumber}, current: ${await aztecNode.getBlockNumber()}`); + + // Advance to exactly msgBlockNumber - 1. At this point: + // - forPublicConsumption: true returns true (blockNumber + 1 >= messageBlockNumber) + // - forPublicConsumption: false returns false (blockNumber < messageBlockNumber) + // This is the window where the bug manifests: the readiness check says "go" but the + // message isn't in the current world state yet. + let currentBlock = await aztecNode.getBlockNumber(); + while (currentBlock < msgBlockNumber - 1) { + await advanceBlock(); + currentBlock = await aztecNode.getBlockNumber(); + } + + // Verify we're in the interesting window (not past it) + expect(currentBlock).toBe(msgBlockNumber - 1); + + const readyForPublic = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: true }); + const readyForPrivate = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: false }); + log.warn(`At block ${currentBlock}: readyForPublic=${readyForPublic}, readyForPrivate=${readyForPrivate}`); + expect(readyForPublic).toBe(true); + expect(readyForPrivate).toBe(false); + + // Simulate public consumption. Before A-548 fix, this fails with: + // "Assertion failed: Tried to consume nonexistent L1-to-L2 message" + // because the simulator runs against the current world state which doesn't have the message. + // After the fix, the simulator includes pending L1-to-L2 messages in the fork. + const consume = testContract.methods.consume_message_from_arbitrary_sender_public( + message.content, + secret, + crossChainTestHarness.ethAccount, + globalLeafIndex, + ); + + await consume.simulate({ from: user1Address }); + + // Also verify the message can actually be consumed by sending the tx + await consume.send({ from: user1Address }); + }, 120_000); + // Inbox block number can drift on two scenarios: if the rollup reorgs and rolls back its own // block number, or if the inbox receives too many messages and they are inserted faster than // they are consumed. In this test, we mine several blocks without marking them as proven until From f8387ae9df082498cf0ce7e163871fd76820038c Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 26 Feb 2026 16:46:17 +0000 Subject: [PATCH 2/9] Fix block based APIs for L1 to L2 messages --- .../archiver/src/store/message_store.ts | 2 +- .../archiver/src/test/mock_l2_block_source.ts | 6 + .../aztec-node/src/aztec-node/server.test.ts | 73 ----------- .../aztec-node/src/aztec-node/server.ts | 40 ++---- .../aztec.js/src/utils/cross_chain.ts | 23 ++-- yarn-project/bot/src/cross_chain_bot.ts | 2 +- yarn-project/bot/src/factory.ts | 6 +- .../src/bench/node_rpc_perf.test.ts | 14 +- .../l1_to_l2.test.ts | 120 ++++++++---------- .../src/spartan/setup_test_wallets.ts | 2 +- .../stdlib/src/block/l2_block_source.ts | 6 + .../stdlib/src/interfaces/archiver.test.ts | 3 + .../stdlib/src/interfaces/archiver.ts | 1 + .../stdlib/src/interfaces/aztec-node.test.ts | 16 ++- .../stdlib/src/interfaces/aztec-node.ts | 18 ++- 15 files changed, 132 insertions(+), 200 deletions(-) diff --git a/yarn-project/archiver/src/store/message_store.ts b/yarn-project/archiver/src/store/message_store.ts index 4c07ba9f9c86..0408f7f0c3c2 100644 --- a/yarn-project/archiver/src/store/message_store.ts +++ b/yarn-project/archiver/src/store/message_store.ts @@ -137,7 +137,7 @@ export class MessageStore { ); } - // Check the first message in a block has the correct index. + // Check the first message in a checkpoint has the correct index. if ( (!lastMessage || message.checkpointNumber > lastMessage.checkpointNumber) && message.index !== expectedStart diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index ff4a0fe4af52..da295c09cb96 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -42,6 +42,12 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { await this.createCheckpoints(numBlocks, 1); } + public getCheckpointNumber(): Promise { + return Promise.resolve( + this.checkpointList.length === 0 ? CheckpointNumber.ZERO : CheckpointNumber(this.checkpointList.length), + ); + } + /** Creates checkpoints, each containing `blocksPerCheckpoint` blocks. */ public async createCheckpoints(numCheckpoints: number, blocksPerCheckpoint: number = 1) { for (let c = 0; c < numCheckpoints; c++) { 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 9e62e110b2cf..e930d8c4852f 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -412,79 +412,6 @@ describe('aztec node', () => { unfreeze(tx.data.constants.txContext.gasSettings.gasLimits).l2Gas = 1e12; await expect(node.simulatePublicCalls(tx)).rejects.toThrow(/gas/i); }); - - it('appends pending L1-to-L2 messages to the simulation fork', async () => { - lastBlockNumber = BlockNumber(5); - - // Mock buildGlobalVariables so the method doesn't crash before reaching the fork - globalVariablesBuilder.buildGlobalVariables.mockResolvedValue( - GlobalVariables.empty({ blockNumber: BlockNumber(6) }), - ); - - // Mock getBlockData to return block data with a checkpoint number - const blockData: BlockData = { - header: BlockHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - blockHash: Fr.ZERO, - checkpointNumber: CheckpointNumber(5), - indexWithinCheckpoint: IndexWithinCheckpoint(0), - }; - l2BlockSource.getBlockData.mockResolvedValue(blockData); - - // Mock L1-to-L2 messages for the next checkpoint - const pendingMessages = [Fr.random(), Fr.random(), Fr.random()]; - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(pendingMessages); - - // Mock the fork to track appendLeaves calls - const merkleTreeFork = mock(); - worldState.fork.mockResolvedValue(merkleTreeFork); - - // simulatePublicCalls will fail during actual processing since we don't set up a - // full processor, but we only care about verifying that appendLeaves was called - const tx = await mockTxForRollup(0x10000); - await node.simulatePublicCalls(tx).catch(() => {}); - - // Verify that appendLeaves was called with the pending messages padded to the correct size - expect(merkleTreeFork.appendLeaves).toHaveBeenCalledWith( - MerkleTreeId.L1_TO_L2_MESSAGE_TREE, - padArrayEnd(pendingMessages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), - ); - - // Verify the messages were fetched for the NEXT checkpoint (current + 1) - expect(l1ToL2MessageSource.getL1ToL2Messages).toHaveBeenCalledWith(CheckpointNumber(6)); - - // Verify fork was closed - expect(merkleTreeFork.close).toHaveBeenCalled(); - }); - - it('skips appending when there are no pending L1-to-L2 messages', async () => { - lastBlockNumber = BlockNumber(3); - - globalVariablesBuilder.buildGlobalVariables.mockResolvedValue( - GlobalVariables.empty({ blockNumber: BlockNumber(4) }), - ); - - const blockData: BlockData = { - header: BlockHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - blockHash: Fr.ZERO, - checkpointNumber: CheckpointNumber(3), - indexWithinCheckpoint: IndexWithinCheckpoint(0), - }; - l2BlockSource.getBlockData.mockResolvedValue(blockData); - - // No pending messages for the next checkpoint - l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]); - - const merkleTreeFork = mock(); - worldState.fork.mockResolvedValue(merkleTreeFork); - - const tx = await mockTxForRollup(0x10000); - await node.simulatePublicCalls(tx).catch(() => {}); - - // appendLeaves should NOT be called when there are no messages - expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled(); - }); }); describe('reloadKeystore', () => { diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 57803f657008..5578b33749a8 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -2,19 +2,14 @@ import { Archiver, createArchiver } from '@aztec/archiver'; import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client'; import { Blob } from '@aztec/blob-lib'; -import { - ARCHIVE_HEIGHT, - type L1_TO_L2_MSG_TREE_HEIGHT, - type NOTE_HASH_TREE_HEIGHT, - NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, -} from '@aztec/constants'; +import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { getPublicClient } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { compactArray, padArrayEnd, pick, unique } from '@aztec/foundation/collection'; +import { compactArray, pick, unique } 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'; @@ -177,7 +172,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { throw new Error('debugLogStore should never be enabled when realProofs are set'); } } - public async getWorldStateSyncStatus(): Promise { const status = await this.worldStateSynchronizer.status(); return status.syncSummary; @@ -739,6 +733,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return await this.blockSource.getCheckpointedL2BlockNumber(); } + public getCheckpointNumber(): Promise { + return this.blockSource.getCheckpointNumber(); + } + /** * Method to fetch the version of the package. * @returns The node package version @@ -1046,11 +1044,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return [witness.index, witness.path]; } - public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise { + public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message); - return messageIndex - ? BlockNumber.fromCheckpointNumber(InboxLeaf.checkpointNumberFromIndex(messageIndex)) - : undefined; + return messageIndex ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined; } /** @@ -1259,26 +1255,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { await this.worldStateSynchronizer.syncImmediate(latestBlockNumber); const merkleTreeFork = await this.worldStateSynchronizer.fork(); try { - // Append pending L1-to-L2 messages for the next checkpoint to the simulation fork. - // This mirrors what the sequencer does in LightweightCheckpointBuilder.startNewCheckpoint(): - // it appends messages to the L1_TO_L2_MESSAGE_TREE BEFORE executing block TXs. Without this, - // simulations that consume L1-to-L2 messages fail because the messages aren't in the tree yet. - // See https://linear.app/aztec-labs/issue/A-548 for details. - const blockData = await this.blockSource.getBlockData(latestBlockNumber); - if (blockData) { - const nextCheckpointNumber = CheckpointNumber(blockData.checkpointNumber + 1); - const pendingL1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(nextCheckpointNumber); - if (pendingL1ToL2Messages.length > 0) { - await merkleTreeFork.appendLeaves( - MerkleTreeId.L1_TO_L2_MESSAGE_TREE, - padArrayEnd(pendingL1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), - ); - this.log.verbose( - `Appended ${pendingL1ToL2Messages.length} pending L1-to-L2 messages for checkpoint ${nextCheckpointNumber} to simulation fork`, - ); - } - } - const config = PublicSimulatorConfig.from({ skipFeeEnforcement, collectDebugLogs: true, diff --git a/yarn-project/aztec.js/src/utils/cross_chain.ts b/yarn-project/aztec.js/src/utils/cross_chain.ts index 38c278fe9597..88e1ee3d3c3f 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.ts @@ -9,16 +9,16 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; * @param opts - Options */ export async function waitForL1ToL2MessageReady( - node: Pick, + node: Pick, l1ToL2MessageHash: Fr, opts: { /** Timeout for the operation in seconds */ timeoutSeconds: number; /** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean; }, ) { - const messageBlockNumber = await node.getL1ToL2MessageBlock(l1ToL2MessageHash); + const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash); return retryUntil( - () => isL1ToL2MessageReady(node, l1ToL2MessageHash, { ...opts, messageBlockNumber }), + () => isL1ToL2MessageReady(node, l1ToL2MessageHash, { ...opts, messageCheckpointNumber }), `L1 to L2 message ${l1ToL2MessageHash.toString()} ready`, opts.timeoutSeconds, 1, @@ -33,21 +33,24 @@ export async function waitForL1ToL2MessageReady( * @returns True if the message is ready to be consumed, false otherwise */ export async function isL1ToL2MessageReady( - node: Pick, + node: Pick, l1ToL2MessageHash: Fr, opts: { /** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean; - /** Cached synced block number for the message (will be fetched from PXE otherwise) */ messageBlockNumber?: number; + /** Cached synced block number for the message (will be fetched from PXE otherwise) */ messageCheckpointNumber?: number; }, ): Promise { - const blockNumber = await node.getBlockNumber(); - const messageBlockNumber = opts.messageBlockNumber ?? (await node.getL1ToL2MessageBlock(l1ToL2MessageHash)); - if (messageBlockNumber === undefined) { + const checkpointNumber = await node.getCheckpointNumber(); + const messageCheckpointNumber = + opts.messageCheckpointNumber ?? (await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash)); + if (messageCheckpointNumber === undefined) { return false; } - // Note that public messages can be consumed 1 block earlier, since the sequencer will include the messages + // Note that public messages can be consumed 1 checkpointNumber earlier, since the sequencer will include the messages // in the L1 to L2 message tree before executing the txs for the block. In private, however, we need to wait // until the message is included so we can make use of the membership witness. - return opts.forPublicConsumption ? blockNumber + 1 >= messageBlockNumber : blockNumber >= messageBlockNumber; + return opts.forPublicConsumption + ? checkpointNumber + 1 >= messageCheckpointNumber + : checkpointNumber >= messageCheckpointNumber; } diff --git a/yarn-project/bot/src/cross_chain_bot.ts b/yarn-project/bot/src/cross_chain_bot.ts index df89ba5e6afb..2226a34d3ca3 100644 --- a/yarn-project/bot/src/cross_chain_bot.ts +++ b/yarn-project/bot/src/cross_chain_bot.ts @@ -175,7 +175,7 @@ export class CrossChainBot extends BaseBot { const now = Date.now(); for (const msg of pendingMessages) { const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash), { - forPublicConsumption: true, + forPublicConsumption: false, }); if (ready) { return msg; diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index d84beb632b5c..89975c5308bf 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -158,7 +158,7 @@ export class BotFactory { const firstMsg = allMessages[0]; await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: true, + forPublicConsumption: false, }); this.log.info(`First L1→L2 message is ready`); } @@ -503,7 +503,7 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, messageHash, { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: true, + forPublicConsumption: false, }), ); return existingClaim.claim; @@ -542,7 +542,7 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: true, + forPublicConsumption: false, }), ); 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 a0362bf60e56..385076c83cdf 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 @@ -280,6 +280,12 @@ describe('e2e_node_rpc_perf', () => { expect(stats.avg).toBeLessThan(1000); }); + it('benchmarks getCheckpointNumber', async () => { + const { stats } = await benchmark('getCheckpointNumber', () => aztecNode.getCheckpointNumber()); + addResult('getCheckpointNumber', stats); + expect(stats.avg).toBeLessThan(1000); + }); + it('benchmarks getProvenBlockNumber', async () => { const { stats } = await benchmark('getProvenBlockNumber', () => aztecNode.getProvenBlockNumber()); addResult('getProvenBlockNumber', stats); @@ -414,10 +420,12 @@ describe('e2e_node_rpc_perf', () => { }); describe('message APIs', () => { - it('benchmarks getL1ToL2MessageBlock', async () => { + it('benchmarks getL1ToL2MessageCheckpoint', async () => { const l1ToL2Message = Fr.random(); - const { stats } = await benchmark('getL1ToL2MessageBlock', () => aztecNode.getL1ToL2MessageBlock(l1ToL2Message)); - addResult('getL1ToL2MessageBlock', stats); + const { stats } = await benchmark('getL1ToL2MessageCheckpoint', () => + aztecNode.getL1ToL2MessageCheckpoint(l1ToL2Message), + ); + addResult('getL1ToL2MessageCheckpoint', stats); expect(stats.avg).toBeLessThan(2000); }); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index 6cb7b619ec8a..4fae726d5bfd 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -56,7 +56,34 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { if (newBlock === block) { throw new Error(`Failed to advance block ${block}`); } - return undefined; + return newBlock; + }; + + const waitForBlockToCheckpoint = async (blockNumber: BlockNumber) => { + return await retryUntil( + async () => { + const checkpointedBlockNumber = await aztecNode.getCheckpointedBlockNumber(); + const isCheckpointed = checkpointedBlockNumber >= blockNumber; + if (!isCheckpointed) { + return undefined; + } + const [checkpointedBlock] = await aztecNode.getCheckpointedBlocks(blockNumber, 1); + return checkpointedBlock.checkpointNumber; + }, + 'wait for block to checkpoint', + 60, + ); + }; + + const advanceCheckpoint = async () => { + let checkpoint = await aztecNode.getCheckpointNumber(); + const originalcheckpoint = checkpoint; + log.warn(`Original checkpoint ${originalcheckpoint}`); + do { + const newBlock = await advanceBlock(); + checkpoint = await waitForBlockToCheckpoint(newBlock); + } while (checkpoint <= originalcheckpoint); + log.warn(`At checkpoint ${checkpoint}`); }; // Same as above but ignores errors. Useful if we expect a prune. @@ -72,8 +99,8 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { const waitForMessageFetched = async (msgHash: Fr) => { log.warn(`Waiting until the message is fetched by the node`); return await retryUntil( - async () => (await aztecNode.getL1ToL2MessageBlock(msgHash)) ?? (await advanceBlock()), - 'get msg block', + async () => (await aztecNode.getL1ToL2MessageCheckpoint(msgHash)) ?? (await advanceBlock()), + 'get msg checkpoint', 60, ); }; @@ -84,20 +111,27 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { scope: 'private' | 'public', onNotReady?: (blockNumber: BlockNumber) => Promise, ) => { - const msgBlock = await waitForMessageFetched(msgHash); - log.warn(`Waiting until L2 reaches msg block ${msgBlock} (current is ${await aztecNode.getBlockNumber()})`); + const msgCheckpoint = await waitForMessageFetched(msgHash); + log.warn( + `Waiting until L2 reaches msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getL1ToL2MessageCheckpoint(msgHash)})`, + ); await retryUntil( async () => { - const blockNumber = await aztecNode.getBlockNumber(); + const [blockNumber, checkpointNumber] = await Promise.all([ + aztecNode.getBlockNumber(), + aztecNode.getCheckpointNumber(), + ]); const witness = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash); const isReady = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: scope === 'public' }); - log.info(`Block is ${blockNumber}. Message block is ${msgBlock}. Witness ${!!witness}. Ready ${isReady}.`); + log.info( + `Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message checkpoint is ${msgCheckpoint}. Witness ${!!witness}. Ready ${isReady}.`, + ); if (!isReady) { await (onNotReady ? onNotReady(blockNumber) : advanceBlock()); } return isReady; }, - `wait for rollup to reach msg block ${msgBlock}`, + `wait for rollup to reach msg checkpoint ${msgCheckpoint}`, 120, ); }; @@ -162,72 +196,22 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { 120_000, ); - // Regression test for A-548: when isL1ToL2MessageReady returns true with forPublicConsumption: true, - // the simulator should be able to simulate a tx that consumes the message. Before the fix, - // simulation would revert because the public VM ran against the current world state which didn't - // yet contain the message (it would only be added by the sequencer when building the next block). - it('can simulate public consumption of L1 to L2 message when forPublicConsumption is true', async () => { - // Send L1 to L2 message - const [secret, secretHash] = await generateClaimSecret(); - const message = { recipient: testContract.address, content: Fr.random(), secretHash }; - const { msgHash, globalLeafIndex } = await sendL1ToL2Message(message, crossChainTestHarness); - - // Wait for the archiver to sync the message and get the target block number - const msgBlockNumber = await waitForMessageFetched(msgHash); - log.warn(`Message fetched, target block: ${msgBlockNumber}, current: ${await aztecNode.getBlockNumber()}`); - - // Advance to exactly msgBlockNumber - 1. At this point: - // - forPublicConsumption: true returns true (blockNumber + 1 >= messageBlockNumber) - // - forPublicConsumption: false returns false (blockNumber < messageBlockNumber) - // This is the window where the bug manifests: the readiness check says "go" but the - // message isn't in the current world state yet. - let currentBlock = await aztecNode.getBlockNumber(); - while (currentBlock < msgBlockNumber - 1) { - await advanceBlock(); - currentBlock = await aztecNode.getBlockNumber(); - } - - // Verify we're in the interesting window (not past it) - expect(currentBlock).toBe(msgBlockNumber - 1); - - const readyForPublic = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: true }); - const readyForPrivate = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: false }); - log.warn(`At block ${currentBlock}: readyForPublic=${readyForPublic}, readyForPrivate=${readyForPrivate}`); - expect(readyForPublic).toBe(true); - expect(readyForPrivate).toBe(false); - - // Simulate public consumption. Before A-548 fix, this fails with: - // "Assertion failed: Tried to consume nonexistent L1-to-L2 message" - // because the simulator runs against the current world state which doesn't have the message. - // After the fix, the simulator includes pending L1-to-L2 messages in the fork. - const consume = testContract.methods.consume_message_from_arbitrary_sender_public( - message.content, - secret, - crossChainTestHarness.ethAccount, - globalLeafIndex, - ); - - await consume.simulate({ from: user1Address }); - - // Also verify the message can actually be consumed by sending the tx - await consume.send({ from: user1Address }); - }, 120_000); - - // Inbox block number can drift on two scenarios: if the rollup reorgs and rolls back its own - // block number, or if the inbox receives too many messages and they are inserted faster than - // they are consumed. In this test, we mine several blocks without marking them as proven until + // Inbox checkpoint number can drift on two scenarios: if the rollup reorgs and rolls back its own + // checkpoint number, or if the inbox receives too many messages and they are inserted faster than + // they are consumed. In this test, we mine several checkpoints without marking them as proven until // we can trigger a reorg, and then wait until the message can be processed to consume it. it.each(['private', 'public'] as const)( 'can consume L1 to L2 message in %s after inbox drifts away from the rollup', async (scope: 'private' | 'public') => { // Stop proving const lastProven = await aztecNode.getBlockNumber(); - log.warn(`Stopping proof submission at block ${lastProven} to allow drift`); + const [checkpointedProvenBlock] = await aztecNode.getCheckpointedBlocks(lastProven, 1); + log.warn(`Stopping proof submission at checkpoint ${checkpointedProvenBlock.checkpointNumber} to allow drift`); t.context.watcher.setIsMarkingAsProven(false); - // Mine several blocks to ensure drift + // Mine several checkpoints to ensure drift log.warn(`Mining blocks to allow drift`); - await timesAsync(4, advanceBlock); + await timesAsync(4, advanceCheckpoint); // Generate and send the message to the L1 contract log.warn(`Sending L1 to L2 message`); @@ -236,9 +220,9 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { const { msgHash, globalLeafIndex } = await sendL1ToL2Message(message, crossChainTestHarness); // Wait until the Aztec node has synced it - const msgBlockNumber = await waitForMessageFetched(msgHash); - log.warn(`Message synced for block ${msgBlockNumber}`); - expect(lastProven + 4).toBeLessThan(msgBlockNumber); + const msgCheckpointNumber = await waitForMessageFetched(msgHash); + log.warn(`Message synced for checkpoint ${msgCheckpointNumber}`); + expect(checkpointedProvenBlock.checkpointNumber + 4).toBeLessThan(msgCheckpointNumber); // And keep mining until we prune back to the original block number. Now the "waiting for two blocks" // strategy for the message to be ready to use shouldn't work, since the lastProven block is more than diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 8009d474e601..790c19e277a4 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -278,7 +278,7 @@ async function bridgeL1FeeJuice( const claim = await portal.bridgeTokensPublic(recipient, amount, true /* mint */); const isSynced = async () => - (await aztecNode.getL1ToL2MessageBlock(Fr.fromHexString(claim.messageHash))) !== undefined; + (await aztecNode.getL1ToL2MessageCheckpoint(Fr.fromHexString(claim.messageHash))) !== undefined; await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 0.5); log.info(`Created a claim for ${amount} L1 fee juice to ${recipient}.`, claim); diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index f1daf550d7eb..39368d09ad99 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -49,6 +49,12 @@ export interface L2BlockSource { */ getBlockNumber(): Promise; + /** + * Gets the number of the latest L2 checkpoint processed by the block source implementation. + * @returns The number of the latest L2 checkpoint processed by the block source implementation. + */ + getCheckpointNumber(): Promise; + /** * Gets the number of the latest L2 block proven seen by the block source implementation. * @returns The number of the latest L2 block proven seen by the block source implementation. diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 2b5cb983325b..2a7767654aa3 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -405,6 +405,9 @@ class MockArchiver implements ArchiverApi { getCheckpointedL2BlockNumber(): Promise { return Promise.resolve(BlockNumber(1)); } + getCheckpointNumber(): Promise { + return Promise.resolve(CheckpointNumber(1)); + } getFinalizedL2BlockNumber(): Promise { return Promise.resolve(BlockNumber(0)); } diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 9af2b49e6fbc..949c66575040 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -86,6 +86,7 @@ export const ArchiverApiSchema: ApiSchemaFor = { getBlockNumber: z.function().args().returns(BlockNumberSchema), getProvenBlockNumber: z.function().args().returns(BlockNumberSchema), getCheckpointedL2BlockNumber: z.function().args().returns(BlockNumberSchema), + getCheckpointNumber: z.function().args().returns(CheckpointNumberSchema), getFinalizedL2BlockNumber: z.function().args().returns(BlockNumberSchema), getBlock: z.function().args(BlockNumberSchema).returns(L2Block.schema.optional()), getBlockHeader: z diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 436945dd773e..9af187bb2436 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -115,8 +115,8 @@ describe('AztecNodeApiSchema', () => { expect(response).toEqual([1n, expect.any(SiblingPath)]); }); - it('getL1ToL2MessageBlock', async () => { - const response = await context.client.getL1ToL2MessageBlock(Fr.random()); + it('getL1ToL2MessageCheckpoint', async () => { + const response = await context.client.getL1ToL2MessageCheckpoint(Fr.random()); expect(response).toEqual(5); }); @@ -209,6 +209,11 @@ describe('AztecNodeApiSchema', () => { expect(response).toBe(BlockNumber(1)); }); + it('getcheckpointNumber', async () => { + const response = await context.client.getCheckpointNumber(); + expect(response).toBe(CheckpointNumber(1)); + }); + it('isReady', async () => { const response = await context.client.isReady(); expect(response).toBe(true); @@ -578,9 +583,9 @@ class MockAztecNode implements AztecNode { expect(noteHash).toBeInstanceOf(Fr); return Promise.resolve(MembershipWitness.random(NOTE_HASH_TREE_HEIGHT)); } - getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise { + getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { expect(l1ToL2Message).toBeInstanceOf(Fr); - return Promise.resolve(BlockNumber(5)); + return Promise.resolve(CheckpointNumber(5)); } isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise { expect(l1ToL2Message).toBeInstanceOf(Fr); @@ -658,6 +663,9 @@ class MockAztecNode implements AztecNode { getCheckpointedBlockNumber(): Promise { return Promise.resolve(BlockNumber(1)); } + getCheckpointNumber(): Promise { + return Promise.resolve(CheckpointNumber(1)); + } isReady(): Promise { return Promise.resolve(true); } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 34b8639eee7f..7f613d9e6891 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -4,7 +4,9 @@ import { BlockNumber, BlockNumberPositiveSchema, BlockNumberSchema, + CheckpointNumber, CheckpointNumberPositiveSchema, + CheckpointNumberSchema, EpochNumber, EpochNumberSchema, type SlotNumber, @@ -172,14 +174,14 @@ export interface AztecNode l1ToL2Message: Fr, ): Promise<[bigint, SiblingPath] | undefined>; - /** Returns the L2 block number in which this L1 to L2 message becomes available, or undefined if not found. */ - getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise; + /** Returns the L2 checkpoint number in which this L1 to L2 message becomes available, or undefined if not found. */ + getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise; /** * Returns whether an L1 to L2 message is synced by archiver. * @param l1ToL2Message - The L1 to L2 message to check. * @returns Whether the message is synced. - * @deprecated Use `getL1ToL2MessageBlock` instead. This method may return true even if the message is not ready to use. + * @deprecated Use `getL1ToL2MessageCheckpoint` instead. This method may return true even if the message is not ready to use. */ isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise; @@ -230,6 +232,12 @@ export interface AztecNode */ getCheckpointedBlockNumber(): Promise; + /** + * Method to fetch the latest checkpoint number synchronized by the node. + * @returns The checkpoint number. + */ + getCheckpointNumber(): Promise; + /** * Method to determine if the node is ready to accept transactions. * @returns - Flag indicating the readiness for tx submission. @@ -517,7 +525,7 @@ export const AztecNodeApiSchema: ApiSchemaFor = { .args(BlockParameterSchema, schemas.Fr) .returns(z.tuple([schemas.BigInt, SiblingPath.schemaFor(L1_TO_L2_MSG_TREE_HEIGHT)]).optional()), - getL1ToL2MessageBlock: z.function().args(schemas.Fr).returns(BlockNumberSchema.optional()), + getL1ToL2MessageCheckpoint: z.function().args(schemas.Fr).returns(CheckpointNumberSchema.optional()), isL1ToL2MessageSynced: z.function().args(schemas.Fr).returns(z.boolean()), @@ -534,6 +542,8 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getBlockNumber: z.function().returns(BlockNumberSchema), + getCheckpointNumber: z.function().returns(CheckpointNumberSchema), + getProvenBlockNumber: z.function().returns(BlockNumberSchema), getCheckpointedBlockNumber: z.function().returns(BlockNumberSchema), From e6160e52593be4ccd8951db5ad0d5cb3f66036cc Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 26 Feb 2026 17:15:05 +0000 Subject: [PATCH 3/9] Removed old apis --- .../aztec.js/src/utils/cross_chain.ts | 19 +++---------------- yarn-project/bot/src/cross_chain_bot.ts | 4 +--- yarn-project/bot/src/factory.ts | 3 --- .../l1_to_l2.test.ts | 2 +- .../e2e_epochs/epochs_mbps.parallel.test.ts | 2 +- .../epochs_proof_public_cross_chain.test.ts | 1 - 6 files changed, 6 insertions(+), 25 deletions(-) diff --git a/yarn-project/aztec.js/src/utils/cross_chain.ts b/yarn-project/aztec.js/src/utils/cross_chain.ts index 88e1ee3d3c3f..2e0fcd27608c 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.ts @@ -13,12 +13,10 @@ export async function waitForL1ToL2MessageReady( l1ToL2MessageHash: Fr, opts: { /** Timeout for the operation in seconds */ timeoutSeconds: number; - /** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean; }, ) { - const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash); return retryUntil( - () => isL1ToL2MessageReady(node, l1ToL2MessageHash, { ...opts, messageCheckpointNumber }), + () => isL1ToL2MessageReady(node, l1ToL2MessageHash), `L1 to L2 message ${l1ToL2MessageHash.toString()} ready`, opts.timeoutSeconds, 1, @@ -29,28 +27,17 @@ export async function waitForL1ToL2MessageReady( * Returns whether the L1 to L2 message is ready to be consumed. * @param node - Aztec node instance used to obtain the information about the message * @param l1ToL2MessageHash - Hash of the L1 to L2 message - * @param opts - Options * @returns True if the message is ready to be consumed, false otherwise */ export async function isL1ToL2MessageReady( node: Pick, l1ToL2MessageHash: Fr, - opts: { - /** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean; - /** Cached synced block number for the message (will be fetched from PXE otherwise) */ messageCheckpointNumber?: number; - }, ): Promise { const checkpointNumber = await node.getCheckpointNumber(); - const messageCheckpointNumber = - opts.messageCheckpointNumber ?? (await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash)); + const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash); if (messageCheckpointNumber === undefined) { return false; } - // Note that public messages can be consumed 1 checkpointNumber earlier, since the sequencer will include the messages - // in the L1 to L2 message tree before executing the txs for the block. In private, however, we need to wait - // until the message is included so we can make use of the membership witness. - return opts.forPublicConsumption - ? checkpointNumber + 1 >= messageCheckpointNumber - : checkpointNumber >= messageCheckpointNumber; + return checkpointNumber >= messageCheckpointNumber; } diff --git a/yarn-project/bot/src/cross_chain_bot.ts b/yarn-project/bot/src/cross_chain_bot.ts index 2226a34d3ca3..52c59a54f58d 100644 --- a/yarn-project/bot/src/cross_chain_bot.ts +++ b/yarn-project/bot/src/cross_chain_bot.ts @@ -174,9 +174,7 @@ export class CrossChainBot extends BaseBot { ): Promise { const now = Date.now(); for (const msg of pendingMessages) { - const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash), { - forPublicConsumption: false, - }); + const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash)); if (ready) { return msg; } diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 89975c5308bf..89b7ed4be7da 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -158,7 +158,6 @@ export class BotFactory { const firstMsg = allMessages[0]; await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: false, }); this.log.info(`First L1→L2 message is ready`); } @@ -503,7 +502,6 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, messageHash, { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: false, }), ); return existingClaim.claim; @@ -542,7 +540,6 @@ export class BotFactory { await this.withNoMinTxsPerBlock(() => waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), { timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds, - forPublicConsumption: false, }), ); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index 4fae726d5bfd..0f7ae30411a8 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -122,7 +122,7 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { aztecNode.getCheckpointNumber(), ]); const witness = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash); - const isReady = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: scope === 'public' }); + const isReady = await isL1ToL2MessageReady(aztecNode, msgHash); log.info( `Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message checkpoint is ${msgCheckpoint}. Witness ${!!witness}. Ready ${isReady}.`, ); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts index 1917f419e9f4..50154c9bdb0b 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_mbps.parallel.test.ts @@ -356,7 +356,7 @@ describe('e2e_epochs/epochs_mbps', () => { l1ToL2Messages.map(async ({ msgHash }, i) => { logger.warn(`Waiting for L1→L2 message ${i + 1} to be ready`); await retryUntil( - () => isL1ToL2MessageReady(context.aztecNode, msgHash, { forPublicConsumption: true }), + () => isL1ToL2MessageReady(context.aztecNode, msgHash), `L1→L2 message ${i + 1} ready`, test.L2_SLOT_DURATION_IN_S * 5, ); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts index 846cd5f82b96..699930ba4395 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_proof_public_cross_chain.test.ts @@ -57,7 +57,6 @@ describe('e2e_epochs/epochs_proof_public_cross_chain', () => { logger.warn(`Waiting for message ${msgHash} with index ${globalLeafIndex} to be synced`); await waitForL1ToL2MessageReady(context.aztecNode, msgHash, { - forPublicConsumption: true, timeoutSeconds: test.L2_SLOT_DURATION_IN_S * 6, }); From c7543d38953422a84f256ccff7a35687e1d0ee74 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 26 Feb 2026 17:32:30 +0000 Subject: [PATCH 4/9] Review changes --- .../aztec-node/src/aztec-node/server.test.ts | 26 +++++-------------- .../aztec-node/src/aztec-node/server.ts | 1 + .../l1_to_l2.test.ts | 17 ++++++++---- 3 files changed, 19 insertions(+), 25 deletions(-) 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 e930d8c4852f..17d6f3f51928 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1,9 +1,7 @@ import { TestCircuitVerifier } from '@aztec/bb-prover'; -import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import { EpochCache } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; -import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; -import { padArrayEnd } from '@aztec/foundation/collection'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { BadRequestError } from '@aztec/foundation/json-rpc'; @@ -18,24 +16,14 @@ import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-ju import type { GlobalVariableBuilder, SequencerClient } from '@aztec/sequencer-client'; import type { SlasherClientInterface } from '@aztec/slasher'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type BlockData, L2Block, type L2BlockSource } from '@aztec/stdlib/block'; +import { L2Block, type L2BlockSource } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; -import type { - L2LogsSource, - MerkleTreeReadOperations, - MerkleTreeWriteOperations, - WorldStateSynchronizer, -} from '@aztec/stdlib/interfaces/server'; +import type { L2LogsSource, MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { mockTx } from '@aztec/stdlib/testing'; -import { - AppendOnlyTreeSnapshot, - MerkleTreeId, - PublicDataTreeLeaf, - PublicDataTreeLeafPreimage, -} from '@aztec/stdlib/trees'; +import { MerkleTreeId, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees'; import { BlockHeader, GlobalVariables, @@ -79,8 +67,6 @@ describe('aztec node', () => { let globalVariablesBuilder: MockProxy; let merkleTreeOps: MockProxy; let l2BlockSource: MockProxy; - let l1ToL2MessageSource: MockProxy; - let worldState: MockProxy; let lastBlockNumber: BlockNumber; let node: AztecNodeService; let feePayer: AztecAddress; @@ -144,7 +130,7 @@ describe('aztec node', () => { } }); - worldState = mock({ + const worldState = mock({ getCommitted: () => merkleTreeOps, }); @@ -153,7 +139,7 @@ describe('aztec node', () => { const l2LogsSource = mock(); - l1ToL2MessageSource = mock(); + const l1ToL2MessageSource = mock(); // all txs use the same allowed FPC class const contractSource = mock(); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 5578b33749a8..0b971ec8082b 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -172,6 +172,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { throw new Error('debugLogStore should never be enabled when realProofs are set'); } } + public async getWorldStateSyncStatus(): Promise { const status = await this.worldStateSynchronizer.status(); return status.syncSummary; diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index 0f7ae30411a8..c05487e6a98e 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -77,12 +77,12 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { const advanceCheckpoint = async () => { let checkpoint = await aztecNode.getCheckpointNumber(); - const originalcheckpoint = checkpoint; - log.warn(`Original checkpoint ${originalcheckpoint}`); + const originalCheckpoint = checkpoint; + log.warn(`Original checkpoint ${originalCheckpoint}`); do { const newBlock = await advanceBlock(); checkpoint = await waitForBlockToCheckpoint(newBlock); - } while (checkpoint <= originalcheckpoint); + } while (checkpoint <= originalCheckpoint); log.warn(`At checkpoint ${checkpoint}`); }; @@ -95,11 +95,18 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { } }; - // Waits until the message is fetched by the archiver of the node and returns the msg target block + // Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint const waitForMessageFetched = async (msgHash: Fr) => { log.warn(`Waiting until the message is fetched by the node`); return await retryUntil( - async () => (await aztecNode.getL1ToL2MessageCheckpoint(msgHash)) ?? (await advanceBlock()), + async () => { + const checkpoint = await aztecNode.getL1ToL2MessageCheckpoint(msgHash); + if (checkpoint !== undefined) { + return checkpoint; + } + await advanceBlock(); + return undefined; + }, 'get msg checkpoint', 60, ); From cc00302c008c2a392f46677feda6843156452bfa Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 26 Feb 2026 17:44:27 +0000 Subject: [PATCH 5/9] Fix --- yarn-project/aztec.js/src/utils/cross_chain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/aztec.js/src/utils/cross_chain.ts b/yarn-project/aztec.js/src/utils/cross_chain.ts index 2e0fcd27608c..d5292db76f23 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.ts @@ -8,7 +8,7 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; * @param l1ToL2MessageHash - Hash of the L1 to L2 message * @param opts - Options */ -export async function waitForL1ToL2MessageReady( +export function waitForL1ToL2MessageReady( node: Pick, l1ToL2MessageHash: Fr, opts: { From f6c90003256cd03a2282d671ee7c3f32fcde94a0 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 26 Feb 2026 18:00:01 +0000 Subject: [PATCH 6/9] Test fix --- yarn-project/stdlib/src/interfaces/archiver.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 2a7767654aa3..710118ceb5e9 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -81,6 +81,11 @@ describe('ArchiverApiSchema', () => { expect(result).toEqual(BlockNumber(1)); }); + it('getCheckpointNumber', async () => { + const result = await context.client.getCheckpointNumber(); + expect(result).toEqual(CheckpointNumber(1)); + }); + it('getProvenBlockNumber', async () => { const result = await context.client.getProvenBlockNumber(); expect(result).toEqual(BlockNumber(1)); From 9860107a44a762a66a9eda0c6bc9d9d311b07c9d Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 3 Mar 2026 13:42:01 +0000 Subject: [PATCH 7/9] Signal that L1 to L2 message is ready at first block in a checkpoint --- .../aztec.js/src/utils/cross_chain.ts | 9 ++-- .../l1_to_l2.test.ts | 50 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/yarn-project/aztec.js/src/utils/cross_chain.ts b/yarn-project/aztec.js/src/utils/cross_chain.ts index d5292db76f23..5ba97b49c758 100644 --- a/yarn-project/aztec.js/src/utils/cross_chain.ts +++ b/yarn-project/aztec.js/src/utils/cross_chain.ts @@ -9,7 +9,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; @@ -30,14 +30,15 @@ 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, ): Promise { - const checkpointNumber = await node.getCheckpointNumber(); const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash); if (messageCheckpointNumber === undefined) { return false; } - return checkpointNumber >= messageCheckpointNumber; + // L1 to L2 messages are included in the first block of a checkpoint + const latestBlock = await node.getBlock('latest'); + return latestBlock !== undefined && latestBlock.checkpointNumber >= messageCheckpointNumber; } diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index c05487e6a98e..010745880b87 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -6,7 +6,7 @@ import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import type { AztecNode } from '@aztec/aztec.js/node'; import { TxExecutionResult } from '@aztec/aztec.js/tx'; import type { Wallet } from '@aztec/aztec.js/wallet'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types'; import { timesAsync } from '@aztec/foundation/collection'; import { retryUntil } from '@aztec/foundation/retry'; import { TestContract } from '@aztec/noir-test-contracts.js/Test'; @@ -120,7 +120,7 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { ) => { const msgCheckpoint = await waitForMessageFetched(msgHash); log.warn( - `Waiting until L2 reaches msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getL1ToL2MessageCheckpoint(msgHash)})`, + `Waiting until L2 reaches the first block of msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getCheckpointNumber()})`, ); await retryUntil( async () => { @@ -159,12 +159,8 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { await waitForMessageReady(message1Hash, scope); - // The waitForMessageReady returns true earlier for public-land, so we can only check the membership - // witness for private-land here. - if (scope === 'private') { - const [message1Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message1Hash))!; - expect(actualMessage1Index.toBigInt()).toBe(message1Index); - } + const [message1Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message1Hash))!; + expect(actualMessage1Index.toBigInt()).toBe(message1Index); // We consume the L1 to L2 message using the test contract either from private or public await getConsumeMethod(scope)( @@ -184,12 +180,10 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { // We check that the duplicate message was correctly inserted by checking that its message index is defined await waitForMessageReady(message2Hash, scope); - if (scope === 'private') { - const [message2Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message2Hash))!; - expect(message2Index).toBeDefined(); - expect(message2Index).toBeGreaterThan(actualMessage1Index.toBigInt()); - expect(actualMessage2Index.toBigInt()).toBe(message2Index); - } + const [message2Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message2Hash))!; + expect(message2Index).toBeDefined(); + expect(message2Index).toBeGreaterThan(actualMessage1Index.toBigInt()); + expect(actualMessage2Index.toBigInt()).toBe(message2Index); // Now we consume the message again. Everything should pass because oracle should return the duplicate message // which is not nullified @@ -251,30 +245,38 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { getConsumeMethod(scope)(message.content, secret, crossChainTestHarness.ethAccount, globalLeafIndex); // Wait until the message is ready to be consumed, checking that it cannot be consumed beforehand - await waitForMessageReady(msgHash, scope, async () => { + await waitForMessageReady(msgHash, scope, async (blockNumber: BlockNumber) => { if (scope === 'private') { // On private, we simulate the tx locally and check that we get a missing message error, then we advance to the next block await expect(() => consume().simulate({ from: user1Address })).rejects.toThrow(/No L1 to L2 message found/); await tryAdvanceBlock(); - await t.context.watcher.markAsProven(); } else { - // On public, we actually send the tx and check that it reverts due to the missing message. - // This advances the block too as a side-effect. Note that we do not rely on a simulation since the cross chain messages - // do not get added at the beginning of the block during node_simulatePublicCalls (maybe they should?). + // In public it is harder to determine when a message becomes consumable. + // We send a transaction, this advances the chain and the message MIGHT be consumed in the new block. + // If it does get consumed then we check that the block contains the message. + // If it fails we check that the block doesn't contain the message const receipt = await consume().send({ from: user1Address, wait: { dontThrowOnRevert: true } }); - expect(receipt.executionResult).toEqual(TxExecutionResult.APP_LOGIC_REVERTED); - await t.context.watcher.markAsProven(); + if (receipt.executionResult === TxExecutionResult.SUCCESS) { + // The block the transaction included should be for the message checkpoint number + // and be the first block in the checkpoint + const block = await aztecNode.getBlock(receipt.blockNumber!); + expect(block).toBeDefined(); + expect(block!.checkpointNumber).toEqual(msgCheckpointNumber); + expect(block!.indexWithinCheckpoint).toEqual(IndexWithinCheckpoint.ZERO); + } else { + expect(receipt.executionResult).toEqual(TxExecutionResult.APP_LOGIC_REVERTED); + } } + await t.context.watcher.markAsProven(); }); // Verify the membership witness is available for creating the tx (private-land only) if (scope === 'private') { const [messageIndex] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash))!; expect(messageIndex).toEqual(globalLeafIndex.toBigInt()); + // And consume the message for private, public was already consumed. + await consume().send({ from: user1Address }); } - - // And consume the message - await consume().send({ from: user1Address }); }, ); }); From 7124de940bd6ec2e416d725cce0fe670d0ac4b5a Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 3 Mar 2026 13:51:21 +0000 Subject: [PATCH 8/9] Fix --- .../end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts index 010745880b87..ec5ecbe78b57 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts @@ -245,7 +245,7 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => { getConsumeMethod(scope)(message.content, secret, crossChainTestHarness.ethAccount, globalLeafIndex); // Wait until the message is ready to be consumed, checking that it cannot be consumed beforehand - await waitForMessageReady(msgHash, scope, async (blockNumber: BlockNumber) => { + await waitForMessageReady(msgHash, scope, async () => { if (scope === 'private') { // On private, we simulate the tx locally and check that we get a missing message error, then we advance to the next block await expect(() => consume().simulate({ from: user1Address })).rejects.toThrow(/No L1 to L2 message found/); From 04b26d1ea46cdc6a3b40ad0ceb9cf71d9a1aacb1 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 3 Mar 2026 16:23:38 +0000 Subject: [PATCH 9/9] Test fix --- yarn-project/stdlib/src/interfaces/aztec-node.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 9af187bb2436..9afac73c16d5 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -209,7 +209,7 @@ describe('AztecNodeApiSchema', () => { expect(response).toBe(BlockNumber(1)); }); - it('getcheckpointNumber', async () => { + it('getCheckpointNumber', async () => { const response = await context.client.getCheckpointNumber(); expect(response).toBe(CheckpointNumber(1)); });