Skip to content

Commit c394466

Browse files
committed
chore(fast-inbox): remove world-state padding remnants and node simulator message fetch (A-1388)
Drops the no-op first-in-checkpoint padding alias and its stale docs in world-state, and removes the obsolete non-first-block-empty-bundle transitional test. The node public-calls simulator no longer fetches next-checkpoint messages via the removed per-checkpoint API and drops its now-unused l1ToL2MessageSource dependency, simulating against the fork's current tree (streaming Inbox consumes per block). Rewrites the THREAT_MODEL inHash/consume passages to the consensus rolling-hash / bucket model.
1 parent 70213e7 commit c394466

7 files changed

Lines changed: 20 additions & 103 deletions

File tree

yarn-project/THREAT_MODEL.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,9 @@ L1 does **not** verify committee attestations at propose time (gas optimization)
203203
invalid attestations is rejected without touching potentially malformed blob data
204204
([validation.ts](archiver/src/modules/validation.ts)).
205205
- **C2 — Sync never bricks.** The archiver skips invalid checkpoints, advances its syncpoint past them, and keeps
206-
processing. The `inHash` check cannot be weaponized: L1 enforces `header.inHash == inbox.consume(...)` at propose
207-
time, so a local mismatch indicates a node bug, not attacker-controlled input (`ProposeLib`).
206+
processing. The consensus rolling-hash check cannot be weaponized: L1 enforces at propose time that the checkpoint
207+
header's `inboxRollingHash` matches the rolling hash of the Inbox bucket the checkpoint consumes through, so a local
208+
mismatch indicates a node bug, not attacker-controlled input (`ProposeLib`).
208209
- **C3 — Bad-attestation checkpoints are removable and unprovable.** Anyone can call `invalidateBadAttestation` /
209210
`invalidateInsufficientAttestations` to purge them (no rebate; expected callers: next proposer, then committee, then
210211
any validator, with timed fallbacks in the sequencer). They cannot enter the proven chain: `submitEpochRootProof`
@@ -236,8 +237,8 @@ L1 does **not** verify committee attestations at propose time (gas optimization)
236237

237238
- **S1 — Sync everything provable.** Nodes must be able to sync from L1 any checkpoint that provers can prove, even if
238239
local validator policy would have refused to attest to it. Validator-side caps (`VALIDATOR_MAX_*`) apply only to p2p
239-
proposal validation; the archiver's L1 path enforces only attestation validity, `inHash` consistency, and structural
240-
blob decoding. Rationale: escape-hatch and malicious-quorum checkpoints bypass validator policy but can still reach
240+
proposal validation; the archiver's L1 path enforces only attestation validity, consensus rolling-hash consistency,
241+
and structural blob decoding. Rationale: escape-hatch and malicious-quorum checkpoints bypass validator policy but can still reach
241242
the proven chain, and a node that refuses to sync them forks itself off.
242243
- **S2 — L1 reorg resilience.** Message and checkpoint sync detect L1 reorgs (rolling hashes, archive root comparison),
243244
unwind to the common ancestor, and re-fetch — including the subtle case of checkpoints added *behind* the syncpoint.

yarn-project/aztec-node/src/aztec-node/node_public_calls_simulator.test.ts

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { L1ToL2MessagesNotReadyError } from '@aztec/archiver';
21
import type { EpochCacheInterface } from '@aztec/epoch-cache';
32
import { type FeeHeader, RollupContract } from '@aztec/ethereum/contracts';
43
import {
@@ -26,10 +25,9 @@ import type { ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
2625
import type { ContractDataSource } from '@aztec/stdlib/contract';
2726
import { GasFees } from '@aztec/stdlib/gas';
2827
import type { MerkleTreeWriteOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
29-
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
3028
import { CheckpointHeader } from '@aztec/stdlib/rollup';
3129
import { mockTx } from '@aztec/stdlib/testing';
32-
import { AppendOnlyTreeSnapshot, MerkleTreeId } from '@aztec/stdlib/trees';
30+
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
3331
import {
3432
BlockHeader,
3533
type CheckpointGlobalVariables,
@@ -50,7 +48,6 @@ const ROLLUP_ADDRESS = EthAddress.random();
5048
describe('NodePublicCallsSimulator', () => {
5149
let blockSource: MockProxy<L2BlockSource>;
5250
let worldStateSynchronizer: MockProxy<WorldStateSynchronizer>;
53-
let l1ToL2MessageSource: MockProxy<L1ToL2MessageSource>;
5451
let contractDataSource: MockProxy<ContractDataSource>;
5552
let globalVariableBuilder: MockProxy<GlobalVariableBuilder>;
5653
let rollupContract: MockProxy<RollupContract>;
@@ -127,7 +124,6 @@ describe('NodePublicCallsSimulator', () => {
127124

128125
blockSource = mock<L2BlockSource>();
129126
worldStateSynchronizer = mock<WorldStateSynchronizer>();
130-
l1ToL2MessageSource = mock<L1ToL2MessageSource>();
131127
contractDataSource = mock<ContractDataSource>();
132128
globalVariableBuilder = mock<GlobalVariableBuilder>();
133129
rollupContract = mock<RollupContract>();
@@ -140,7 +136,6 @@ describe('NodePublicCallsSimulator', () => {
140136
(merkleTreeFork as unknown as { [Symbol.asyncDispose]: () => Promise<void> })[Symbol.asyncDispose] = () =>
141137
Promise.resolve();
142138
worldStateSynchronizer.fork.mockResolvedValue(merkleTreeFork);
143-
l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]);
144139
blockSource.getPendingChainValidationStatus.mockResolvedValue({ valid: true });
145140
blockSource.getProposedCheckpointData.mockResolvedValue(undefined);
146141

@@ -168,7 +163,6 @@ describe('NodePublicCallsSimulator', () => {
168163
simulator = new NodePublicCallsSimulator({
169164
blockSource,
170165
worldStateSynchronizer,
171-
l1ToL2MessageSource,
172166
contractDataSource,
173167
globalVariableBuilder,
174168
rollupContract,
@@ -232,7 +226,6 @@ describe('NodePublicCallsSimulator', () => {
232226

233227
await simulator.simulate(tx);
234228

235-
expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled();
236229
expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled();
237230
});
238231

@@ -245,8 +238,7 @@ describe('NodePublicCallsSimulator', () => {
245238

246239
await expect(simulator.simulate(tx)).rejects.toThrow();
247240

248-
// Must not treat the next block as opening a new checkpoint and re-insert the ongoing checkpoint's messages.
249-
expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled();
241+
// Must not treat the next block as opening a new checkpoint.
250242
expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled();
251243
expect(globalVariableBuilder.buildCheckpointGlobalVariables).not.toHaveBeenCalled();
252244
});
@@ -281,35 +273,17 @@ describe('NodePublicCallsSimulator', () => {
281273
expect(plan?.chainTipsOverride).toEqual({ pending: CheckpointNumber(1), proven: CheckpointNumber(1) });
282274
});
283275

284-
it('inserts L1-to-L2 messages for the next checkpoint', async () => {
276+
it('does not insert L1-to-L2 messages when opening a new checkpoint', async () => {
285277
const tx = await lowGasTx();
286-
const messages = [Fr.fromString('0x1234'), Fr.fromString('0x5678')];
287278
blockSource.getL2Tips.mockResolvedValue(setupBoundary());
288279
blockSource.getBlockData.mockImplementation((query: BlockQuery) =>
289280
Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(5)) : undefined),
290281
);
291282
mockNextL1Slot(SlotNumber(20));
292-
l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue(messages);
293-
294-
await simulator.simulate(tx);
295-
296-
// targetCheckpoint = proposedCheckpoint.number + 1
297-
expect(l1ToL2MessageSource.getL1ToL2Messages).toHaveBeenCalledWith(CheckpointNumber(2));
298-
const [treeId, appended] = merkleTreeFork.appendLeaves.mock.calls[0];
299-
expect(treeId).toEqual(MerkleTreeId.L1_TO_L2_MESSAGE_TREE);
300-
expect(appended.slice(0, 2)).toEqual(messages);
301-
});
302-
303-
it('tolerates L1ToL2MessagesNotReadyError and simulates without messages', async () => {
304-
const tx = await lowGasTx();
305-
blockSource.getL2Tips.mockResolvedValue(setupBoundary());
306-
blockSource.getBlockData.mockImplementation((query: BlockQuery) =>
307-
Promise.resolve('number' in query ? makeBlockData(query.number, SlotNumber(5)) : undefined),
308-
);
309-
mockNextL1Slot(SlotNumber(20));
310-
l1ToL2MessageSource.getL1ToL2Messages.mockRejectedValue(new L1ToL2MessagesNotReadyError(CheckpointNumber(2), 0n));
311283

312284
await expect(simulator.simulate(tx)).resolves.toBeDefined();
285+
286+
// Streaming Inbox: the next checkpoint's messages are consumed per block, so none are appended here.
313287
expect(merkleTreeFork.appendLeaves).not.toHaveBeenCalled();
314288
});
315289

@@ -408,7 +382,6 @@ describe('NodePublicCallsSimulator', () => {
408382
new NodePublicCallsSimulator({
409383
blockSource,
410384
worldStateSynchronizer,
411-
l1ToL2MessageSource,
412385
contractDataSource,
413386
globalVariableBuilder,
414387
epochCache,

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

Lines changed: 7 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { L1ToL2MessagesNotReadyError } from '@aztec/archiver';
21
import { PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
32
import type { EpochCacheInterface } from '@aztec/epoch-cache';
43
import {
@@ -8,20 +7,17 @@ import {
87
} from '@aztec/ethereum/contracts';
98
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
109
import { compactArray } from '@aztec/foundation/collection';
11-
import { Fr } from '@aztec/foundation/curves/bn254';
1210
import { EthAddress } from '@aztec/foundation/eth-address';
1311
import { BadRequestError } from '@aztec/foundation/json-rpc';
1412
import { type Logger, createLogger } from '@aztec/foundation/log';
1513
import { DateProvider } from '@aztec/foundation/timer';
16-
import { isErrorClass } from '@aztec/foundation/types';
1714
import { type AvmSimulator, PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server';
1815
import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
1916
import { AztecAddress } from '@aztec/stdlib/aztec-address';
2017
import type { L2BlockSource, L2Tips } from '@aztec/stdlib/block';
2118
import { type ProposedCheckpointData, buildCheckpointSimulationOverridesPlan } from '@aztec/stdlib/checkpoint';
2219
import type { ContractDataSource } from '@aztec/stdlib/contract';
2320
import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
24-
import { type L1ToL2MessageSource, appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
2521
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
2622
import {
2723
type GlobalVariableBuilder,
@@ -46,7 +42,6 @@ export interface NodePublicCallsSimulatorConfig {
4642
export interface NodePublicCallsSimulatorDeps {
4743
blockSource: L2BlockSource;
4844
worldStateSynchronizer: WorldStateSynchronizer;
49-
l1ToL2MessageSource: L1ToL2MessageSource;
5045
contractDataSource: ContractDataSource;
5146
globalVariableBuilder: GlobalVariableBuilder;
5247
/**
@@ -87,7 +82,6 @@ export interface NodePublicCallsSimulatorDeps {
8782
export class NodePublicCallsSimulator {
8883
private readonly blockSource: L2BlockSource;
8984
private readonly worldStateSynchronizer: WorldStateSynchronizer;
90-
private readonly l1ToL2MessageSource: L1ToL2MessageSource;
9185
private readonly contractDataSource: ContractDataSource;
9286
private readonly globalVariableBuilder: GlobalVariableBuilder;
9387
private readonly rollupContract: RollupContract | undefined;
@@ -101,7 +95,6 @@ export class NodePublicCallsSimulator {
10195
constructor(deps: NodePublicCallsSimulatorDeps) {
10296
this.blockSource = deps.blockSource;
10397
this.worldStateSynchronizer = deps.worldStateSynchronizer;
104-
this.l1ToL2MessageSource = deps.l1ToL2MessageSource;
10598
this.contractDataSource = deps.contractDataSource;
10699
this.globalVariableBuilder = deps.globalVariableBuilder;
107100
this.rollupContract = deps.rollupContract;
@@ -157,9 +150,7 @@ export class NodePublicCallsSimulator {
157150
// the proposed-checkpoint terminating block; it opens a new checkpoint when they coincide.
158151
const atCheckpointBoundary = proposedCheckpointLastBlock === l2Tips.proposed.number;
159152

160-
// `targetCheckpoint` is the checkpoint whose L1-to-L2 messages must be inserted into the fork
161-
// before simulation. Only set when opening a new checkpoint, where the next block is its first block.
162-
const { globalVariables: newGlobalVariables, targetCheckpoint } = atCheckpointBoundary
153+
const { globalVariables: newGlobalVariables } = atCheckpointBoundary
163154
? await this.buildGlobalVariablesForNewCheckpoint(l2Tips, proposedCheckpointData, blockNumber)
164155
: { globalVariables: await this.copyGlobalVariablesFromLatestProposedBlock(latestBlockNumber, blockNumber) };
165156

@@ -184,19 +175,12 @@ export class NodePublicCallsSimulator {
184175
// Ensure world-state has caught up with the latest block we loaded from the archiver
185176
await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
186177

187-
const nextCheckpointMessages = await this.getNextCheckpointMessages(targetCheckpoint);
188-
189-
// 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
178+
// Request a new fork of the world state at the latest block number, and apply any overrides to it before
179+
// simulation. The next checkpoint's L1-to-L2 messages are not inserted here: under the streaming Inbox
180+
// (AZIP-22 Fast Inbox) a checkpoint's messages are consumed per block, so the simulation runs against the
181+
// fork's current tree without predicting the next block's message bundle.
190182
await using merkleTreeFork = await this.worldStateSynchronizer.fork(latestBlockNumber);
191183

192-
if (nextCheckpointMessages !== undefined) {
193-
this.log.debug(
194-
`Appending ${nextCheckpointMessages.length} L1-to-L2 messages to the world state tree for the next checkpoint`,
195-
{ checkpointNumber: targetCheckpoint },
196-
);
197-
await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages);
198-
}
199-
200184
await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
201185

202186
const config = PublicSimulatorConfig.from({
@@ -235,37 +219,6 @@ export class NodePublicCallsSimulator {
235219
);
236220
}
237221

238-
/**
239-
* Fetches the next checkpoint's L1-to-L2 messages to insert into the fork before simulation. Only set
240-
* when opening a new checkpoint; when continuing an in-progress checkpoint the ongoing checkpoint's
241-
* messages were already applied when its first block synced, so inserting here would double-count them
242-
* — which is why a missing header for the latest proposed block throws rather than falling through to
243-
* this path. A not-ready or failed fetch degrades to simulating without the messages rather than
244-
* failing the request.
245-
*/
246-
private async getNextCheckpointMessages(targetCheckpoint: CheckpointNumber | undefined): Promise<Fr[] | undefined> {
247-
if (targetCheckpoint === undefined) {
248-
return undefined;
249-
}
250-
try {
251-
return await this.l1ToL2MessageSource.getL1ToL2Messages(targetCheckpoint);
252-
} catch (err) {
253-
if (isErrorClass(err, L1ToL2MessagesNotReadyError)) {
254-
this.log.warn(
255-
`L1-to-L2 messages for checkpoint ${targetCheckpoint} are not ready yet (simulating without them)`,
256-
{ checkpointNumber: targetCheckpoint },
257-
);
258-
} else {
259-
this.log.error(
260-
`Failed to get L1-to-L2 messages for checkpoint ${targetCheckpoint} (simulating without them)`,
261-
err,
262-
{ checkpointNumber: targetCheckpoint },
263-
);
264-
}
265-
return undefined;
266-
}
267-
}
268-
269222
/**
270223
* Continues an in-progress checkpoint: the next block extends the checkpoint the latest proposed
271224
* block belongs to. Every block in a checkpoint shares the same `CheckpointGlobalVariables`, so the
@@ -298,19 +251,14 @@ export class NodePublicCallsSimulator {
298251
* sequencer applies so the simulated mana min fee matches what the sequencer will write into the
299252
* block header. Coinbase and fee recipient stay zero (we cannot know the future proposer's payout
300253
* addresses), unlike continuing an in-progress checkpoint which inherits the real ones from the
301-
* proposed header. Returns the target checkpoint so the caller inserts that checkpoint's L1-to-L2
302-
* messages into the fork.
254+
* proposed header.
303255
*/
304256
private async buildGlobalVariablesForNewCheckpoint(
305257
l2Tips: L2Tips,
306258
proposedCheckpointData: ProposedCheckpointData | undefined,
307259
blockNumber: BlockNumber,
308-
): Promise<{ globalVariables: GlobalVariables; targetCheckpoint: CheckpointNumber }> {
260+
): Promise<{ globalVariables: GlobalVariables }> {
309261
const checkpointedCheckpointNumber = l2Tips.checkpointed.checkpoint.number;
310-
// The new checkpoint sits on top of the proposed one when pipelining, otherwise on the
311-
// checkpointed tip. The target slot and the overrides plan both derive from the single
312-
// `proposedCheckpointData` read, so they cannot disagree about the proposed parent.
313-
const proposedCheckpointNumber = proposedCheckpointData?.checkpointNumber ?? checkpointedCheckpointNumber;
314262

315263
const targetSlot = this.computeTargetSlot(proposedCheckpointData);
316264
const plan = await this.buildSimulationOverridesPlan(proposedCheckpointData, checkpointedCheckpointNumber);
@@ -324,7 +272,6 @@ export class NodePublicCallsSimulator {
324272

325273
return {
326274
globalVariables: GlobalVariables.from({ blockNumber, ...checkpointGlobalVariables }),
327-
targetCheckpoint: CheckpointNumber(proposedCheckpointNumber + 1),
328275
};
329276
}
330277

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
241241
this.nodePublicCallsSimulator = new NodePublicCallsSimulator({
242242
blockSource: this.blockSource,
243243
worldStateSynchronizer: this.worldStateSynchronizer,
244-
l1ToL2MessageSource: this.l1ToL2MessageSource,
245244
contractDataSource: this.contractDataSource,
246245
globalVariableBuilder: this.globalVariableBuilder,
247246
rollupContract: this.rollupContract,

yarn-project/prover-node/src/prover-node.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,6 @@ describe('ProverNode', () => {
653653
*/
654654
function setupRegistrationSuccess() {
655655
worldState.syncImmediate.mockResolvedValue(undefined as any);
656-
l1ToL2MessageSource.getL1ToL2Messages.mockResolvedValue([]);
657656
l2BlockSource.getBlockData.mockResolvedValue({
658657
header: { lastArchive: { root: Fr.ZERO }, state: { l1ToL2MessageTree: { nextAvailableLeafIndex: 0 } } },
659658
} as any);

yarn-project/world-state/src/native/native_world_state.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,6 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
266266
public async handleL2BlockAndMessages(l2Block: L2Block, l1ToL2Messages: Fr[]): Promise<WorldStateStatusFull> {
267267
// Any block may carry an L1-to-L2 message bundle and transition the L1-to-L2 message tree by its real (unpadded,
268268
// compact) leaves, matching how the circuits build the tree post-flip (AZIP-22 Fast Inbox).
269-
const paddedL1ToL2Messages = l1ToL2Messages;
270269

271270
// We have to pad the note hashes and nullifiers within tx effects because that's how the trees are built by
272271
// circuits.
@@ -294,7 +293,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase {
294293
// Forwarded so the native sync verifies the archive root against canonical and rejects a divergent tree.
295294
expectedArchiveRoot: l2Block.archive.root.toBuffer(),
296295
expectedPreviousArchiveRoot: l2Block.header.lastArchive.root.toBuffer(),
297-
paddedL1ToL2Messages: paddedL1ToL2Messages.map(serializeLeaf),
296+
paddedL1ToL2Messages: l1ToL2Messages.map(serializeLeaf),
298297
paddedNoteHashes: paddedNoteHashes.map(serializeLeaf),
299298
paddedNullifiers: paddedNullifiers.map(serializeLeaf),
300299
publicDataWrites: publicDataWrites.map(serializeLeaf),

yarn-project/world-state/src/world-state-db/merkle_tree_db.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ export interface MerkleTreeAdminDatabase extends ForkMerkleTreeOperations, Reado
3333
/**
3434
* Handles a single L2 block: inserts its note hashes, nullifiers, public data writes, and the block's L1-to-L2
3535
* message bundle into the merkle trees. Any block may carry a message bundle and transition the L1-to-L2 message
36-
* tree, not just the first block of a checkpoint. A first-in-checkpoint bundle is padded to
37-
* MAX_L1_TO_L2_MSGS_PER_CHECKPOINT to match how the circuits build the tree; a non-first bundle is appended
38-
* exactly as given. Padding is a transitional concern of this method that moves entirely to the caller at the flip.
36+
* tree, not just the first block of a checkpoint. The bundle's real (unpadded, compact) leaves are appended as
37+
* given, matching how the circuits build the tree (AZIP-22 Fast Inbox).
3938
* @param block - The L2 block to handle.
4039
* @param l1ToL2Messages - The L1 to L2 messages for the block.
4140
*/

0 commit comments

Comments
 (0)