Skip to content

Commit e974043

Browse files
spalladinoclaude
andcommitted
feat(fast-inbox): answer message-checkpoint queries from records, not index math (A-1384)
Post-flip, 1024-per-checkpoint index arithmetic is wrong for compact indices, so the node answers from stored records instead (AZIP-22 Fast Inbox): - getL1ToL2MessageCheckpoint binary-searches the block records for the first block whose L1-to-L2 tree leaf count exceeds the message index, returning that block's checkpoint (portal claim helpers depend on this). - prover-node collectRegisterData derives the checkpoint's consumed messages from the Inbox buckets between the parent checkpoint's position and the last block, replacing the legacy per-checkpoint getL1ToL2Messages fetch. The message_store index-formula methods (smallestIndexForCheckpoint etc.) have no correctness-critical live callers left; the public-simulator fetch degrades safely without them. They are retired in FI-18. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3e67b28 commit e974043

3 files changed

Lines changed: 72 additions & 9 deletions

File tree

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import type { ContractDataSource, ContractInstanceWithAddress } from '@aztec/std
3737
import { EmptyL1RollupConstants, type L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
3838
import { GasFees } from '@aztec/stdlib/gas';
3939
import type { L2LogsSource, MerkleTreeReadOperations, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
40-
import { InboxLeaf } from '@aztec/stdlib/messaging';
4140
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
4241
import { CheckpointHeader } from '@aztec/stdlib/rollup';
4342
import { mockTx, randomContractInstanceWithAddress } from '@aztec/stdlib/testing';
@@ -1569,13 +1568,18 @@ describe('aztec node', () => {
15691568
});
15701569

15711570
describe('getL1ToL2MessageCheckpoint', () => {
1572-
it('returns the checkpoint for a message at index 0n', async () => {
1571+
it('returns the checkpoint of the block that consumed the message', async () => {
15731572
const msg = Fr.random();
15741573
l1ToL2MessageSource.getL1ToL2MessageIndex.mockResolvedValue(0n);
1574+
// The first block (checkpoint 1) consumed message index 0: its L1-to-L2 tree leaf count is 1 > 0.
1575+
l2BlockSource.getBlockNumber.mockResolvedValue(BlockNumber(1));
1576+
l2BlockSource.getBlockData.mockResolvedValue({
1577+
checkpointNumber: CheckpointNumber(1),
1578+
header: { state: { l1ToL2MessageTree: { nextAvailableLeafIndex: 1 } } },
1579+
} as unknown as BlockData);
15751580

15761581
const result = await node.getL1ToL2MessageCheckpoint(msg);
1577-
expect(result).toEqual(InboxLeaf.checkpointNumberFromIndex(0n));
1578-
expect(result).not.toBeUndefined();
1582+
expect(result).toEqual(CheckpointNumber(1));
15791583
});
15801584

15811585
it('returns undefined when the message is not found', async () => {

yarn-project/aztec-node/src/modules/node_world_state_queries.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
1+
import {
2+
ARCHIVE_HEIGHT,
3+
INITIAL_L2_BLOCK_NUM,
4+
type L1_TO_L2_MSG_TREE_HEIGHT,
5+
type NOTE_HASH_TREE_HEIGHT,
6+
} from '@aztec/constants';
27
import { BlockNumber, type CheckpointNumber, type EpochNumber } from '@aztec/foundation/branded-types';
38
import { chunkBy } from '@aztec/foundation/collection';
49
import { Fr } from '@aztec/foundation/curves/bn254';
@@ -7,6 +12,7 @@ import { sleep } from '@aztec/foundation/sleep';
712
import { MembershipWitness, type SiblingPath } from '@aztec/foundation/trees';
813
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
914
import {
15+
type BlockData,
1016
type BlockHash,
1117
type BlockParameter,
1218
type DataInBlock,
@@ -16,7 +22,7 @@ import {
1622
} from '@aztec/stdlib/block';
1723
import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
1824
import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
19-
import { InboxLeaf, type L1ToL2MessageSource, type L2ToL1MembershipWitness } from '@aztec/stdlib/messaging';
25+
import type { L1ToL2MessageSource, L2ToL1MembershipWitness } from '@aztec/stdlib/messaging';
2026
import {
2127
MerkleTreeId,
2228
type NullifierLeafPreimage,
@@ -179,7 +185,35 @@ export class NodeWorldStateQueries {
179185

180186
public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
181187
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
182-
return messageIndex !== undefined ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined;
188+
if (messageIndex === undefined) {
189+
return undefined;
190+
}
191+
// Post-flip, an L1-to-L2 message at compact leaf index `i` is consumed by the first block whose L1-to-L2 tree leaf
192+
// count exceeds `i` (leaf counts are monotonic in block number); that block's checkpoint is the answer, sourced
193+
// from the stored block records rather than 1024-per-checkpoint index arithmetic (AZIP-22 Fast Inbox).
194+
const block = await this.#findBlockConsumingL1ToL2MessageIndex(messageIndex);
195+
return block?.checkpointNumber;
196+
}
197+
198+
/** Binary-searches the block records for the first block whose L1-to-L2 tree leaf count exceeds `messageIndex`. */
199+
async #findBlockConsumingL1ToL2MessageIndex(messageIndex: bigint): Promise<BlockData | undefined> {
200+
let lo = INITIAL_L2_BLOCK_NUM;
201+
let hi = await this.blockSource.getBlockNumber();
202+
let result: BlockData | undefined;
203+
while (lo <= hi) {
204+
const mid = lo + Math.floor((hi - lo) / 2);
205+
const block = await this.blockSource.getBlockData({ number: BlockNumber(mid) });
206+
if (block === undefined) {
207+
break;
208+
}
209+
if (BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex) > messageIndex) {
210+
result = block;
211+
hi = mid - 1;
212+
} else {
213+
lo = mid + 1;
214+
}
215+
}
216+
return result;
183217
}
184218

185219
/**

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { getLastSiblingPath } from '@aztec/prover-client/helpers';
1313
import { ChonkCache } from '@aztec/prover-client/orchestrator';
1414
import { type AvmSimulator, PublicProcessorFactory } from '@aztec/simulator/server';
1515
import {
16+
type BlockHeader,
1617
EventDrivenL2BlockStream,
18+
type L2Block,
1719
type L2BlockId,
1820
type L2BlockSource,
1921
type L2BlockStreamEvent,
@@ -349,9 +351,11 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
349351
): Promise<RegisterCheckpointData> {
350352
const previousBlockNumber = BlockNumber(checkpoint.blocks[0].number - 1);
351353
const previousBlockHeader = await this.gatherPreviousBlockHeader(previousBlockNumber);
352-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpoint.number);
353-
const previousInboxRollingHash = await this.gatherPreviousInboxRollingHash(checkpoint.number);
354354
const lastBlock = checkpoint.blocks.at(-1)!;
355+
// Streaming Inbox (AZIP-22 Fast Inbox): the checkpoint's consumed messages are those in the Inbox buckets between
356+
// the parent checkpoint's consumed position and this checkpoint's last block (compact leaf-count range).
357+
const l1ToL2Messages = await this.deriveCheckpointConsumedMessages(previousBlockHeader, lastBlock);
358+
const previousInboxRollingHash = await this.gatherPreviousInboxRollingHash(checkpoint.number);
355359
const lastBlockHash = await lastBlock.header.hash();
356360
await this.worldState.syncImmediate(lastBlock.number, lastBlockHash);
357361
const previousArchiveSiblingPath = await getLastSiblingPath(
@@ -367,6 +371,27 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
367371
};
368372
}
369373

374+
/**
375+
* Derives a checkpoint's consumed L1-to-L2 messages, in order, from the Inbox buckets between the parent
376+
* checkpoint's consumed position (the block before the checkpoint's first block) and the checkpoint's last block
377+
* (compact leaf counts), for the prover to slice per block (AZIP-22 Fast Inbox).
378+
*/
379+
private async deriveCheckpointConsumedMessages(previousBlockHeader: BlockHeader, lastBlock: L2Block): Promise<Fr[]> {
380+
const startLeafCount = BigInt(previousBlockHeader.state.l1ToL2MessageTree.nextAvailableLeafIndex);
381+
const endLeafCount = BigInt(lastBlock.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
382+
if (endLeafCount <= startLeafCount) {
383+
return [];
384+
}
385+
const startBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(startLeafCount);
386+
const endBucket = await this.l1ToL2MessageSource.getInboxBucketByTotalMsgCount(endLeafCount);
387+
if (startBucket === undefined || endBucket === undefined) {
388+
throw new Error(
389+
`Cannot resolve consumed messages for checkpoint ending at block ${lastBlock.number} from the Inbox buckets`,
390+
);
391+
}
392+
return this.l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
393+
}
394+
370395
/**
371396
* Sources the inbox rolling hash chain-start for a checkpoint: the previous checkpoint's `inboxRollingHash`, or zero
372397
* for the genesis checkpoint. The prover threads this into the base parity circuits so the rebuilt checkpoint header

0 commit comments

Comments
 (0)