Skip to content

Commit 0d9c391

Browse files
committed
refactor(fast-inbox)!: answer L1-to-L2 message readiness from the message leaf index (A-1388)
Blocks consume Inbox messages in order into consecutive L1-to-L2 tree leaves, so a message is available at a tip exactly when that tip's leaf count has grown past the message's index — one comparison against data every block header already carries. `isL1ToL2MessageReady` now does that via `getL1ToL2MessageIndex`, which also drops its stale claim that messages land in the first block of a checkpoint. The `getL1ToL2MessageCheckpoint` node RPC method existed only to answer this question and needed a binary search over block records to do it, so delete it: node service, world-state queries, interface, schema, and its callers. `getL1ToL2MessageIndex` takes its place in the operator API reference.
1 parent 592e240 commit 0d9c391

11 files changed

Lines changed: 59 additions & 123 deletions

File tree

docs/docs-operate/operators/reference/node-api-reference.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,22 +635,24 @@ curl -X POST http://localhost:8080 \
635635
-d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageMembershipWitness","params":["latest","0x1234..."],"id":1}'
636636
```
637637

638-
### aztec_getL1ToL2MessageCheckpoint
638+
### aztec_getL1ToL2MessageIndex
639639

640-
Returns the L2 checkpoint number in which this L1 to L2 message becomes available, or undefined if not found.
640+
Returns the compact leaf index assigned to this L1 to L2 message as soon as the node has ingested it from L1,
641+
before any L2 block consumes it. Returns undefined if the node has not yet seen the message. The message becomes
642+
consumable once a block's L1-to-L2 message tree grows past this index.
641643

642644
**Parameters**:
643645

644646
1. `l1ToL2Message` - `Fr`
645647

646-
**Returns**: `number | undefined`
648+
**Returns**: `bigint | undefined`
647649

648650
**Example**:
649651

650652
```bash
651653
curl -X POST http://localhost:8080 \
652654
-H 'Content-Type: application/json' \
653-
-d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageCheckpoint","params":["0x1234..."],"id":1}'
655+
-d '{"jsonrpc":"2.0","method":"aztec_getL1ToL2MessageIndex","params":["0x1234..."],"id":1}'
654656
```
655657

656658
### aztec_getL2ToL1MembershipWitness

docs/scripts/node_api_reference_generation/generate_node_api_reference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ const METHOD_GROUPS: { heading: string; namespace: string; methods: string[] }[]
559559
namespace: 'aztec',
560560
methods: [
561561
'getL1ToL2MessageMembershipWitness',
562-
'getL1ToL2MessageCheckpoint',
562+
'getL1ToL2MessageIndex',
563563
'getL2ToL1MembershipWitness',
564564
'getL2ToL1Messages',
565565
],

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,27 +1568,19 @@ describe('aztec node', () => {
15681568
});
15691569
});
15701570

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

1582-
const result = await node.getL1ToL2MessageCheckpoint(msg);
1583-
expect(result).toEqual(CheckpointNumber(1));
1576+
expect(await node.getL1ToL2MessageIndex(msg)).toEqual(7n);
15841577
});
15851578

15861579
it('returns undefined when the message is not found', async () => {
15871580
const msg = Fr.random();
15881581
l1ToL2MessageSource.getL1ToL2MessageIndex.mockResolvedValue(undefined);
15891582

1590-
const result = await node.getL1ToL2MessageCheckpoint(msg);
1591-
expect(result).toBeUndefined();
1583+
expect(await node.getL1ToL2MessageIndex(msg)).toBeUndefined();
15921584
});
15931585
});
15941586

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -673,10 +673,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
673673
return this.worldStateQueries.getL1ToL2MessageMembershipWitness(referenceBlock, l1ToL2Message);
674674
}
675675

676-
public getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
677-
return this.worldStateQueries.getL1ToL2MessageCheckpoint(l1ToL2Message);
678-
}
679-
680676
public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
681677
return this.worldStateQueries.getL1ToL2MessageIndex(l1ToL2Message);
682678
}

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

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
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';
7-
import { BlockNumber, type CheckpointNumber, type EpochNumber } from '@aztec/foundation/branded-types';
1+
import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
2+
import { BlockNumber, type EpochNumber } from '@aztec/foundation/branded-types';
83
import { chunkBy } from '@aztec/foundation/collection';
94
import { Fr } from '@aztec/foundation/curves/bn254';
105
import { type Logger, createLogger } from '@aztec/foundation/log';
116
import { sleep } from '@aztec/foundation/sleep';
127
import { MembershipWitness, type SiblingPath } from '@aztec/foundation/trees';
138
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
149
import {
15-
type BlockData,
1610
type BlockHash,
1711
type BlockParameter,
1812
type DataInBlock,
@@ -187,39 +181,6 @@ export class NodeWorldStateQueries {
187181
return this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
188182
}
189183

190-
public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
191-
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
192-
if (messageIndex === undefined) {
193-
return undefined;
194-
}
195-
// Post-flip, an L1-to-L2 message at compact leaf index `i` is consumed by the first block whose L1-to-L2 tree leaf
196-
// count exceeds `i` (leaf counts are monotonic in block number); that block's checkpoint is the answer, sourced
197-
// from the stored block records rather than 1024-per-checkpoint index arithmetic (AZIP-22 Fast Inbox).
198-
const block = await this.#findBlockConsumingL1ToL2MessageIndex(messageIndex);
199-
return block?.checkpointNumber;
200-
}
201-
202-
/** Binary-searches the block records for the first block whose L1-to-L2 tree leaf count exceeds `messageIndex`. */
203-
async #findBlockConsumingL1ToL2MessageIndex(messageIndex: bigint): Promise<BlockData | undefined> {
204-
let lo: number = INITIAL_L2_BLOCK_NUM;
205-
let hi: number = await this.blockSource.getBlockNumber();
206-
let result: BlockData | undefined;
207-
while (lo <= hi) {
208-
const mid = lo + Math.floor((hi - lo) / 2);
209-
const block = await this.blockSource.getBlockData({ number: BlockNumber(mid) });
210-
if (block === undefined) {
211-
break;
212-
}
213-
if (BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex) > messageIndex) {
214-
result = block;
215-
hi = mid - 1;
216-
} else {
217-
lo = mid + 1;
218-
}
219-
}
220-
return result;
221-
}
222-
223184
/**
224185
* Returns all the L2 to L1 messages in an epoch (empty array if the epoch is not found). The public
225186
* `AztecNodeService.getL2ToL1Messages` that delegates here is deprecated in favor of

yarn-project/aztec.js/src/utils/cross_chain.test.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { CheckpointNumber } from '@aztec/foundation/branded-types';
21
import { Fr } from '@aztec/foundation/curves/bn254';
32
import type { BlockData } from '@aztec/stdlib/block';
43
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
@@ -8,44 +7,45 @@ import { type MockProxy, mock } from 'jest-mock-extended';
87
import { isL1ToL2MessageReady } from './cross_chain.js';
98

109
describe('isL1ToL2MessageReady', () => {
11-
let node: MockProxy<Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageCheckpoint'>>;
10+
let node: MockProxy<Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>>;
1211
let messageHash: Fr;
1312

14-
const blockAtCheckpoint = (checkpointNumber: number) =>
15-
({ checkpointNumber: CheckpointNumber(checkpointNumber) }) as BlockData;
13+
/** A block whose L1-to-L2 message tree holds `leafCount` leaves, i.e. leaf indices 0..leafCount-1. */
14+
const blockWithMessageLeaves = (leafCount: number) =>
15+
({ header: { state: { l1ToL2MessageTree: { nextAvailableLeafIndex: leafCount } } } }) as BlockData;
1616

1717
beforeEach(() => {
1818
node = mock();
1919
messageHash = Fr.random();
2020
});
2121

22-
it('returns false when the message is not yet in any checkpoint', async () => {
23-
node.getL1ToL2MessageCheckpoint.mockResolvedValue(undefined);
22+
it('returns false when the node has not seen the message yet', async () => {
23+
node.getL1ToL2MessageIndex.mockResolvedValue(undefined);
2424

2525
expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false);
2626
expect(node.getBlockData).not.toHaveBeenCalled();
2727
});
2828

2929
describe('latest fallback (no chain tip)', () => {
3030
beforeEach(() => {
31-
node.getL1ToL2MessageCheckpoint.mockResolvedValue(CheckpointNumber(5));
31+
node.getL1ToL2MessageIndex.mockResolvedValue(5n);
3232
});
3333

34-
it('checks readiness against the latest block', async () => {
35-
node.getBlockData.mockResolvedValue(blockAtCheckpoint(5));
34+
it('returns true once the latest block has consumed the message leaf', async () => {
35+
node.getBlockData.mockResolvedValue(blockWithMessageLeaves(6));
3636

3737
expect(await isL1ToL2MessageReady(node, messageHash)).toBe(true);
3838
expect(node.getBlockData).toHaveBeenCalledWith('latest');
3939
});
4040

41-
it('returns true once the latest block reaches the message checkpoint', async () => {
42-
node.getBlockData.mockResolvedValue(blockAtCheckpoint(6));
41+
it('returns false when the latest block stops exactly at the message leaf', async () => {
42+
node.getBlockData.mockResolvedValue(blockWithMessageLeaves(5));
4343

44-
expect(await isL1ToL2MessageReady(node, messageHash)).toBe(true);
44+
expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false);
4545
});
4646

47-
it('returns false when the latest block is behind the message checkpoint', async () => {
48-
node.getBlockData.mockResolvedValue(blockAtCheckpoint(4));
47+
it('returns false when the latest block is behind the message leaf', async () => {
48+
node.getBlockData.mockResolvedValue(blockWithMessageLeaves(4));
4949

5050
expect(await isL1ToL2MessageReady(node, messageHash)).toBe(false);
5151
});
@@ -59,23 +59,23 @@ describe('isL1ToL2MessageReady', () => {
5959

6060
describe('with an explicit chain tip', () => {
6161
beforeEach(() => {
62-
node.getL1ToL2MessageCheckpoint.mockResolvedValue(CheckpointNumber(5));
62+
node.getL1ToL2MessageIndex.mockResolvedValue(5n);
6363
});
6464

6565
it('compares against the requested tip instead of latest', async () => {
66-
// The proven tip lags behind latest: the message is in checkpoint 5 but proven is only at 4.
66+
// The proven tip lags behind latest: latest consumed the message leaf, proven has not.
6767
node.getBlockData.mockImplementation(param =>
68-
Promise.resolve(param === 'proven' ? blockAtCheckpoint(4) : blockAtCheckpoint(6)),
68+
Promise.resolve(param === 'proven' ? blockWithMessageLeaves(5) : blockWithMessageLeaves(7)),
6969
);
7070

7171
expect(await isL1ToL2MessageReady(node, messageHash, 'latest')).toBe(true);
7272
expect(await isL1ToL2MessageReady(node, messageHash, 'proven')).toBe(false);
7373
expect(node.getBlockData).toHaveBeenLastCalledWith('proven');
7474
});
7575

76-
it('returns true once the requested tip reaches the message checkpoint', async () => {
76+
it('returns true once the requested tip has consumed the message leaf', async () => {
7777
node.getBlockData.mockImplementation(param =>
78-
Promise.resolve(param === 'proven' ? blockAtCheckpoint(5) : blockAtCheckpoint(7)),
78+
Promise.resolve(param === 'proven' ? blockWithMessageLeaves(6) : blockWithMessageLeaves(8)),
7979
);
8080

8181
expect(await isL1ToL2MessageReady(node, messageHash, 'proven')).toBe(true);

yarn-project/aztec.js/src/utils/cross_chain.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client';
1010
* @param opts - Options
1111
*/
1212
export function waitForL1ToL2MessageReady(
13-
node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageCheckpoint'>,
13+
node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>,
1414
l1ToL2MessageHash: Fr,
1515
opts: {
1616
/** Timeout for the operation in seconds */ timeoutSeconds: number;
@@ -39,16 +39,17 @@ export function waitForL1ToL2MessageReady(
3939
* @returns True if the message is ready to be consumed, false otherwise
4040
*/
4141
export async function isL1ToL2MessageReady(
42-
node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageCheckpoint'>,
42+
node: Pick<AztecNode, 'getBlockData' | 'getL1ToL2MessageIndex'>,
4343
l1ToL2MessageHash: Fr,
4444
chainTip: BlockTag = 'latest',
4545
): Promise<boolean> {
46-
const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash);
47-
if (messageCheckpointNumber === undefined) {
46+
const messageIndex = await node.getL1ToL2MessageIndex(l1ToL2MessageHash);
47+
if (messageIndex === undefined) {
4848
return false;
4949
}
5050

51-
// L1 to L2 messages are included in the first block of a checkpoint
51+
// Blocks consume L1-to-L2 messages in Inbox order into consecutive leaves of the message tree, so the message is
52+
// available at a tip exactly when that tip's tree has grown past the message's leaf index.
5253
const block = await node.getBlockData(chainTip);
53-
return block !== undefined && block.checkpointNumber >= messageCheckpointNumber;
54+
return block !== undefined && messageIndex < BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
5455
}

yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,10 @@ describe('e2e_node_rpc_perf', () => {
434434
});
435435

436436
describe('message APIs', () => {
437-
it('benchmarks getL1ToL2MessageCheckpoint', async () => {
437+
it('benchmarks getL1ToL2MessageIndex', async () => {
438438
const l1ToL2Message = Fr.random();
439-
const { stats } = await benchmark('getL1ToL2MessageCheckpoint', () =>
440-
aztecNode.getL1ToL2MessageCheckpoint(l1ToL2Message),
441-
);
442-
addResult('getL1ToL2MessageCheckpoint', stats);
439+
const { stats } = await benchmark('getL1ToL2MessageIndex', () => aztecNode.getL1ToL2MessageIndex(l1ToL2Message));
440+
addResult('getL1ToL2MessageIndex', stats);
443441
expect(stats.avg).toBeLessThan(2000);
444442
});
445443

yarn-project/end-to-end/src/single-node/cross-chain/message_test_helpers.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Logger } from '@aztec/aztec.js/log';
44
import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
55
import type { AztecNode } from '@aztec/aztec.js/node';
66
import type { Wallet } from '@aztec/aztec.js/wallet';
7-
import type { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
7+
import type { BlockNumber } from '@aztec/foundation/branded-types';
88
import { retryUntil } from '@aztec/foundation/retry';
99
import { ExecutionPayload } from '@aztec/stdlib/tx';
1010

@@ -33,7 +33,7 @@ export interface L1ToL2MessageHelpers {
3333
secretHash: Fr;
3434
}): ReturnType<typeof sendL1ToL2Message>;
3535
advanceBlock(): Promise<BlockNumber>;
36-
waitForMessageFetched(msgHash: Fr): Promise<CheckpointNumber>;
36+
waitForMessageIndexed(msgHash: Fr): Promise<bigint>;
3737
waitForMessageReady(
3838
msgHash: Fr,
3939
scope: L1ToL2MessageScope,
@@ -72,20 +72,20 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL
7272
return newBlock;
7373
};
7474

75-
// Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint.
76-
// Advances a block on each retry because an L1->L2 message is only indexed once further L2 blocks build.
77-
const waitForMessageFetched = async (msgHash: Fr) => {
75+
// Waits until the node's archiver has ingested the message from the Inbox and returns its message-tree leaf index.
76+
// Advances a block on each retry to keep the chain moving while the archiver catches up with L1.
77+
const waitForMessageIndexed = async (msgHash: Fr) => {
7878
log.warn(`Waiting until the message is fetched by the node`);
7979
return await retryUntil(
8080
async () => {
81-
const checkpoint = await aztecNode.getL1ToL2MessageCheckpoint(msgHash);
82-
if (checkpoint !== undefined) {
83-
return checkpoint;
81+
const messageIndex = await aztecNode.getL1ToL2MessageIndex(msgHash);
82+
if (messageIndex !== undefined) {
83+
return messageIndex;
8484
}
8585
await advanceBlock();
8686
return undefined;
8787
},
88-
'get msg checkpoint',
88+
'get msg index',
8989
60,
9090
);
9191
};
@@ -96,9 +96,9 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL
9696
scope: L1ToL2MessageScope,
9797
onNotReady?: (blockNumber: BlockNumber) => Promise<void>,
9898
) => {
99-
const msgCheckpoint = await waitForMessageFetched(msgHash);
99+
const msgIndex = await waitForMessageIndexed(msgHash);
100100
log.warn(
101-
`Waiting until L2 reaches the first block of msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getCheckpointNumber()})`,
101+
`Waiting until L2 consumes msg leaf index ${msgIndex} (checkpoint is ${await aztecNode.getCheckpointNumber()})`,
102102
);
103103
await retryUntil(
104104
async () => {
@@ -109,17 +109,17 @@ export function createL1ToL2MessageHelpers(deps: L1ToL2MessageHelperDeps): L1ToL
109109
const witness = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash);
110110
const isReady = await isL1ToL2MessageReady(aztecNode, msgHash, t.pxeSyncChainTip);
111111
log.info(
112-
`Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message checkpoint is ${msgCheckpoint}. Witness ${!!witness}. Ready ${isReady}.`,
112+
`Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message leaf index is ${msgIndex}. Witness ${!!witness}. Ready ${isReady}.`,
113113
);
114114
if (!isReady) {
115115
await (onNotReady ? onNotReady(blockNumber) : advanceBlock());
116116
}
117117
return isReady;
118118
},
119-
`wait for rollup to reach msg checkpoint ${msgCheckpoint}`,
119+
`wait for rollup to consume msg leaf index ${msgIndex}`,
120120
240,
121121
);
122122
};
123123

124-
return { sendMessageToL2, advanceBlock, waitForMessageFetched, waitForMessageReady };
124+
return { sendMessageToL2, advanceBlock, waitForMessageIndexed, waitForMessageReady };
125125
}

yarn-project/stdlib/src/interfaces/aztec-node.test.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,6 @@ describe('AztecNodeApiSchema', () => {
154154
expect(response).toEqual([1n, expect.any(SiblingPath)]);
155155
});
156156

157-
it('getL1ToL2MessageCheckpoint', async () => {
158-
const response = await context.client.getL1ToL2MessageCheckpoint(Fr.random());
159-
expect(response).toEqual(5);
160-
});
161-
162157
it('getL1ToL2MessageIndex', async () => {
163158
const response = await context.client.getL1ToL2MessageIndex(Fr.random());
164159
expect(response).toEqual(5n);
@@ -715,10 +710,6 @@ class MockAztecNode implements AztecNode {
715710
expect(noteHash).toBeInstanceOf(Fr);
716711
return Promise.resolve(MembershipWitness.random(NOTE_HASH_TREE_HEIGHT));
717712
}
718-
getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
719-
expect(l1ToL2Message).toBeInstanceOf(Fr);
720-
return Promise.resolve(CheckpointNumber(5));
721-
}
722713
getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
723714
expect(l1ToL2Message).toBeInstanceOf(Fr);
724715
return Promise.resolve(5n);

0 commit comments

Comments
 (0)