Skip to content

Commit 324e498

Browse files
committed
fix(aztec-node): include upcoming checkpoint's L1→L2 messages in simulatePublicCalls
simulatePublicCalls forks the world state at the latest synced block but never inserted the L1→L2 messages that would be added at the start of the next checkpoint. Public AVM opcodes l1_to_l2_msg_exists and consume_l1_to_l2_message read from the fork's L1_TO_L2_MESSAGE_TREE, so simulations diverged from on-chain execution when the next block opened a new checkpoint. The fix derives the pipelining-aware target slot via the epoch cache, compares it against the latest block's slot, and on a slot crossing fetches and appends the next checkpoint's messages onto the simulation fork. The fork is now pinned to the captured latest block number to avoid drift between syncImmediate and fork. The pad-and-append pattern is also extracted into a shared appendL1ToL2MessagesToTree helper in @aztec/stdlib/messaging and adopted by the four existing inline duplicates. A new resumeCheckpoint guard rejects the empty-blocks case explicitly. The end-to-end test for public L1→L2 message consumption now also exercises the simulate path.
1 parent 4d8791a commit 324e498

7 files changed

Lines changed: 77 additions & 41 deletions

File tree

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { BBCircuitVerifier, BatchChonkVerifier, QueuedIVCVerifier } from '@aztec
33
import { TestCircuitVerifier } from '@aztec/bb-prover/test';
44
import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client';
55
import { Blob } from '@aztec/blob-lib';
6-
import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
6+
import {
7+
ARCHIVE_HEIGHT,
8+
INITIAL_L2_BLOCK_NUM,
9+
type L1_TO_L2_MSG_TREE_HEIGHT,
10+
type NOTE_HASH_TREE_HEIGHT,
11+
} from '@aztec/constants';
712
import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
813
import { createEthereumChain } from '@aztec/ethereum/chain';
914
import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client';
@@ -99,7 +104,7 @@ import {
99104
} from '@aztec/stdlib/interfaces/server';
100105
import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
101106
import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
102-
import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
107+
import { InboxLeaf, type L1ToL2MessageSource, appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
103108
import type { Offense } from '@aztec/stdlib/slashing';
104109
import type { NullifierLeafPreimage, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
105110
import { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
@@ -1484,8 +1489,32 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
14841489

14851490
// Ensure world-state has caught up with the latest block we loaded from the archiver
14861491
await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
1487-
const merkleTreeFork = await this.worldStateSynchronizer.fork();
1492+
1493+
// Compute the slot a proposer would actually target under pipelining, then compare with the
1494+
// latest block's slot: if they differ the simulated block opens a new checkpoint, and we must
1495+
// mirror the L1→L2 message insertion the on-chain proposer will perform so that AVM opcodes
1496+
// l1_to_l2_msg_exists / consume_l1_to_l2_message see the same tree state as on-chain.
1497+
const { slot: targetSlot } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
1498+
const latestBlockData = await this.blockSource.getBlockData({ number: latestBlockNumber });
1499+
const isGenesis = latestBlockNumber === BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
1500+
if (latestBlockData === undefined && !isGenesis) {
1501+
throw new Error(`Failed to load block data for latest block ${latestBlockNumber}`);
1502+
}
1503+
const isNewCheckpoint = isGenesis || targetSlot > latestBlockData!.header.getSlot();
1504+
const nextCheckpointMessages = isNewCheckpoint
1505+
? await this.l1ToL2MessageSource.getL1ToL2Messages(
1506+
CheckpointNumber((latestBlockData?.checkpointNumber ?? CheckpointNumber.ZERO) + 1),
1507+
)
1508+
: undefined;
1509+
1510+
// Pin the fork to the captured `latestBlockNumber` so background sync advancing between
1511+
// `syncImmediate` and `fork` cannot leave the fork at a newer block than our checkpoint
1512+
// boundary calculation.
1513+
const merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber);
14881514
try {
1515+
if (nextCheckpointMessages !== undefined) {
1516+
await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages);
1517+
}
14891518
await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
14901519
const config = PublicSimulatorConfig.from({
14911520
skipFeeEnforcement,

yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,19 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
166166
const [message1Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message1Hash))!;
167167
expect(actualMessage1Index.toBigInt()).toBe(message1Index);
168168

169+
const consumeAndSend = async (index: Fr) => {
170+
const call = getConsumeMethod(scope)(message.content, secret, crossChainTestHarness.ethAccount, index);
171+
if (scope === 'public') {
172+
// Public consumption simulates through aztecNode.simulatePublicCalls; this exercises
173+
// the path that mirrors the upcoming checkpoint's L1→L2 message insertion onto the
174+
// simulation fork.
175+
await call.simulate({ from: user1Address });
176+
}
177+
await call.send({ from: user1Address });
178+
};
179+
169180
// We consume the L1 to L2 message using the test contract either from private or public
170-
await getConsumeMethod(scope)(
171-
message.content,
172-
secret,
173-
crossChainTestHarness.ethAccount,
174-
actualMessage1Index,
175-
).send({ from: user1Address });
181+
await consumeAndSend(actualMessage1Index);
176182

177183
// We send and consume the exact same message the second time to test that oracles correctly return the new
178184
// non-nullified message
@@ -191,12 +197,7 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
191197

192198
// Now we consume the message again. Everything should pass because oracle should return the duplicate message
193199
// which is not nullified
194-
await getConsumeMethod(scope)(
195-
message.content,
196-
secret,
197-
crossChainTestHarness.ethAccount,
198-
actualMessage2Index,
199-
).send({ from: user1Address });
200+
await consumeAndSend(actualMessage2Index);
200201
},
201202
120_000,
202203
);

yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { SpongeBlob, computeBlobsHashFromBlobs, encodeCheckpointEndMarker, getBlobsPerL1Block } from '@aztec/blob-lib';
2-
import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants';
32
import { type CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
4-
import { padArrayEnd } from '@aztec/foundation/collection';
53
import { Fr } from '@aztec/foundation/curves/bn254';
64
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
75
import { elapsed } from '@aztec/foundation/timer';
@@ -10,6 +8,7 @@ import { Checkpoint } from '@aztec/stdlib/checkpoint';
108
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
119
import {
1210
accumulateCheckpointOutHashes,
11+
appendL1ToL2MessagesToTree,
1312
computeCheckpointOutHash,
1413
computeInHashFromL1ToL2Messages,
1514
} from '@aztec/stdlib/messaging';
@@ -69,10 +68,7 @@ export class LightweightCheckpointBuilder {
6968
feeAssetPriceModifier: bigint = 0n,
7069
): Promise<LightweightCheckpointBuilder> {
7170
// Insert l1-to-l2 messages into the tree.
72-
await db.appendLeaves(
73-
MerkleTreeId.L1_TO_L2_MESSAGE_TREE,
74-
padArrayEnd<Fr, number>(l1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP),
75-
);
71+
await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
7672

7773
return new LightweightCheckpointBuilder(
7874
checkpointNumber,
@@ -117,6 +113,10 @@ export class LightweightCheckpointBuilder {
117113
blockNumbers: existingBlocks.map(b => b.header.getBlockNumber()),
118114
});
119115

116+
if (existingBlocks.length === 0) {
117+
throw new Error(`Cannot resume checkpoint ${checkpointNumber} with no existing blocks`);
118+
}
119+
120120
// Validate block order and consistency
121121
for (let i = 1; i < existingBlocks.length; i++) {
122122
const prev = existingBlocks[i - 1];

yarn-project/prover-client/src/orchestrator/orchestrator.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import {
33
L1_TO_L2_MSG_SUBTREE_HEIGHT,
44
L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH,
55
NESTED_RECURSIVE_ROLLUP_HONK_PROOF_LENGTH,
6-
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
76
NUM_BASE_PARITY_PER_ROOT_PARITY,
87
} from '@aztec/constants';
98
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
10-
import { padArrayEnd } from '@aztec/foundation/collection';
119
import { Fr } from '@aztec/foundation/curves/bn254';
1210
import { AbortError } from '@aztec/foundation/error';
1311
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
@@ -27,6 +25,7 @@ import type {
2725
ReadonlyWorldStateAccess,
2826
ServerCircuitProver,
2927
} from '@aztec/stdlib/interfaces/server';
28+
import { appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
3029
import type { Proof } from '@aztec/stdlib/proofs';
3130
import {
3231
type BaseRollupHints,
@@ -642,21 +641,14 @@ export class ProvingOrchestrator implements EpochProver {
642641
}
643642

644643
private async updateL1ToL2MessageTree(l1ToL2Messages: Fr[], db: MerkleTreeWriteOperations) {
645-
const l1ToL2MessagesPadded = padArrayEnd<Fr, number>(
646-
l1ToL2Messages,
647-
Fr.ZERO,
648-
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
649-
'Too many L1 to L2 messages',
650-
);
651-
652644
const lastL1ToL2MessageTreeSnapshot = await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, db);
653645
const lastL1ToL2MessageSubtreeRootSiblingPath = assertLength(
654646
await getSubtreeSiblingPath(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, L1_TO_L2_MSG_SUBTREE_HEIGHT, db),
655647
L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH,
656648
);
657649

658650
// Update the local trees to include the new l1 to l2 messages
659-
await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPadded);
651+
await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
660652

661653
const newL1ToL2MessageTreeSnapshot = await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, db);
662654
const newL1ToL2MessageSubtreeRootSiblingPath = assertLength(

yarn-project/prover-node/src/job/epoch-proving-job.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants';
21
import { asyncPool } from '@aztec/foundation/async-pool';
32
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
4-
import { padArrayEnd } from '@aztec/foundation/collection';
53
import { Fr } from '@aztec/foundation/curves/bn254';
64
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
75
import { RunningPromise, promiseWithResolvers } from '@aztec/foundation/promise';
@@ -20,8 +18,8 @@ import {
2018
EpochProvingJobTerminalState,
2119
type ForkMerkleTreeOperations,
2220
} from '@aztec/stdlib/interfaces/server';
21+
import { appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
2322
import { CheckpointConstantData } from '@aztec/stdlib/rollup';
24-
import { MerkleTreeId } from '@aztec/stdlib/trees';
2523
import type { ProcessedTx, Tx } from '@aztec/stdlib/tx';
2624
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
2725

@@ -345,13 +343,7 @@ export class EpochProvingJob implements Traceable {
345343
blockNumber,
346344
l1ToL2Messages: l1ToL2Messages.map(m => m.toString()),
347345
});
348-
const l1ToL2MessagesPadded = padArrayEnd<Fr, number>(
349-
l1ToL2Messages,
350-
Fr.ZERO,
351-
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
352-
'Too many L1 to L2 messages',
353-
);
354-
await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPadded);
346+
await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
355347
}
356348

357349
return db;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants';
2+
import { padArrayEnd } from '@aztec/foundation/collection';
3+
import { Fr } from '@aztec/foundation/curves/bn254';
4+
5+
import type { MerkleTreeWriteOperations } from '../interfaces/merkle_tree_operations.js';
6+
import { MerkleTreeId } from '../trees/merkle_tree_id.js';
7+
8+
/**
9+
* Pads `l1ToL2Messages` to `NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP` and appends them to the
10+
* L1→L2 message tree of `db`. Use whenever a fork at "state before the first block of a
11+
* checkpoint" needs to mirror what the world-state synchronizer inserts at sync time.
12+
*/
13+
export async function appendL1ToL2MessagesToTree(db: MerkleTreeWriteOperations, l1ToL2Messages: Fr[]): Promise<void> {
14+
const padded = padArrayEnd<Fr, number>(
15+
l1ToL2Messages,
16+
Fr.ZERO,
17+
NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
18+
'Too many L1 to L2 messages',
19+
);
20+
await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, padded);
21+
}

yarn-project/stdlib/src/messaging/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './append_l1_to_l2_messages.js';
12
export * from './in_hash.js';
23
export * from './inbox_leaf.js';
34
export * from './l1_to_l2_message.js';

0 commit comments

Comments
 (0)