Skip to content

Commit 1cc2caa

Browse files
committed
fix(fast-inbox): override the pipelined parent's consumed inbox total in propose simulation (A-1384)
`ProposeLib.validateInboxConsumption` reads the parent checkpoint's recorded `inboxMsgTotal` and caps `bucket.totalMsgCount - parentTotal` at the per-checkpoint limit. When the sequencer simulates a propose against a parent that is proposed but not yet mined, the parent has no temp-checkpoint-log cell on L1, so that total read as zero and the consumed count came out as the Inbox's entire history — a spurious `Rollup__TooManyInboxMessagesConsumed` revert once the Inbox has accumulated more than one checkpoint's cap of messages. Carry the total on `ProposedCheckpointData` (derived by the archiver from the checkpoint's last block L1-to-L2 leaf count, which is what `propose` records) and feed it into the pending temp-log override alongside the slot number it shares a word with.
1 parent 6182cb6 commit 1cc2caa

11 files changed

Lines changed: 58 additions & 10 deletions

File tree

yarn-project/archiver/src/archiver-store.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ describe('Archiver Store', () => {
462462
totalManaUsed: 100n,
463463
feeAssetPriceModifier: 0n,
464464
});
465+
return block;
465466
}
466467

467468
it('returns the latest proposed entry when called with no args', async () => {
@@ -477,6 +478,13 @@ describe('Archiver Store', () => {
477478
expect(result).toBeUndefined();
478479
});
479480

481+
it('reports the consumed Inbox total of the last block in the checkpoint', async () => {
482+
const block = await addProposedCheckpoint(CheckpointNumber(1), SlotNumber(3), BlockNumber(1));
483+
484+
const result = await archiver.getProposedCheckpointData();
485+
expect(result!.inboxMsgTotal).toBe(BigInt(block.header.state.l1ToL2MessageTree.nextAvailableLeafIndex));
486+
});
487+
480488
it('returns the latest proposed entry for tag=proposed', async () => {
481489
await addProposedCheckpoint(CheckpointNumber(1), SlotNumber(5), BlockNumber(1));
482490

yarn-project/archiver/src/store/block_store.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ type CheckpointStorage = CommonCheckpointStorage & {
125125
type ProposedCheckpointStorage = CommonCheckpointStorage & {
126126
totalManaUsed: string;
127127
feeAssetPriceModifier: string;
128+
inboxMsgTotal: string;
128129
};
129130

130131
export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
@@ -930,6 +931,7 @@ export class BlockStore {
930931
blockCount: stored.blockCount,
931932
totalManaUsed: BigInt(stored.totalManaUsed),
932933
feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
934+
inboxMsgTotal: BigInt(stored.inboxMsgTotal),
933935
};
934936
}
935937

@@ -1361,7 +1363,7 @@ export class BlockStore {
13611363
* Adds a proposed checkpoint to the pending queue.
13621364
* Accepts proposed.checkpointNumber === latestTip + 1, where latestTip is the highest of
13631365
* confirmed and the highest pending checkpoint number.
1364-
* Computes archive and checkpointOutHash from the stored blocks.
1366+
* Computes archive, checkpointOutHash and the consumed Inbox message total from the stored blocks.
13651367
*/
13661368
async addProposedCheckpoint(proposed: ProposedCheckpointInput) {
13671369
return await this.db.transactionAsync(async () => {
@@ -1387,8 +1389,12 @@ export class BlockStore {
13871389
}
13881390
this.validateCheckpointBlocks(blocks, previousBlock);
13891391

1390-
const archive = blocks[blocks.length - 1].archive;
1392+
const lastBlock = blocks[blocks.length - 1];
1393+
const archive = lastBlock.archive;
13911394
const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
1395+
// The last block's L1-to-L2 leaf count is the checkpoint's cumulative consumed Inbox total under compact
1396+
// indexing, which is what `propose` records on L1 for it (AZIP-22 Fast Inbox).
1397+
const inboxMsgTotal = BigInt(lastBlock.header.state.l1ToL2MessageTree.nextAvailableLeafIndex);
13921398

13931399
await this.#proposedCheckpoints.set(proposed.checkpointNumber, {
13941400
header: proposed.header.toBuffer(),
@@ -1399,6 +1405,7 @@ export class BlockStore {
13991405
blockCount: proposed.blockCount,
14001406
totalManaUsed: proposed.totalManaUsed.toString(),
14011407
feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
1408+
inboxMsgTotal: inboxMsgTotal.toString(),
14021409
});
14031410
});
14041411
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ function makeProposedCheckpointData(args: {
438438
feeAssetPriceModifier: 7n,
439439
archive: new AppendOnlyTreeSnapshot(args.archiveRoot ?? Fr.ZERO, 0),
440440
checkpointOutHash: Fr.fromString('0xfeed'),
441+
inboxMsgTotal: 0n,
441442
};
442443
}
443444

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ describe('aztec node', () => {
13951395
blockCount: 1,
13961396
totalManaUsed: 0n,
13971397
feeAssetPriceModifier: 0n,
1398+
inboxMsgTotal: 0n,
13981399
};
13991400
}
14001401

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ describe('CheckpointProposalJob', () => {
451451
blockCount: 1,
452452
totalManaUsed: 5000n,
453453
feeAssetPriceModifier: 100n,
454+
inboxMsgTotal: 0n,
454455
},
455456
});
456457

@@ -650,6 +651,7 @@ describe('CheckpointProposalJob', () => {
650651
blockCount: 1,
651652
totalManaUsed: 5000n,
652653
feeAssetPriceModifier: 100n,
654+
inboxMsgTotal: 0n,
653655
};
654656

655657
job = createCheckpointProposalJob({
@@ -697,6 +699,7 @@ describe('CheckpointProposalJob', () => {
697699
blockCount: 1,
698700
totalManaUsed: 5000n,
699701
feeAssetPriceModifier: 100n,
702+
inboxMsgTotal: 0n,
700703
};
701704

702705
job = createCheckpointProposalJob({ targetSlot, targetEpoch, proposedCheckpointData });
@@ -848,6 +851,7 @@ describe('CheckpointProposalJob', () => {
848851
blockCount: 1,
849852
totalManaUsed: 5000n,
850853
feeAssetPriceModifier: 100n,
854+
inboxMsgTotal: 0n,
851855
};
852856

853857
let mismatchEvents: { slot: SlotNumber; checkpointNumber: CheckpointNumber; reason: string }[];

yarn-project/sequencer-client/src/sequencer/sequencer.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,7 @@ describe('sequencer', () => {
15751575
blockCount: 1,
15761576
totalManaUsed: 0n,
15771577
feeAssetPriceModifier: 0n,
1578+
inboxMsgTotal: 0n,
15781579
});
15791580

15801581
await sequencer.work();
@@ -1700,6 +1701,7 @@ describe('sequencer', () => {
17001701
blockCount: 1,
17011702
totalManaUsed: 0n,
17021703
feeAssetPriceModifier: 0n,
1704+
inboxMsgTotal: 0n,
17031705
});
17041706

17051707
await sequencer.work();
@@ -1829,6 +1831,7 @@ describe('sequencer', () => {
18291831
blockCount: 1,
18301832
totalManaUsed: 0n,
18311833
feeAssetPriceModifier: 0n,
1834+
inboxMsgTotal: 0n,
18321835
},
18331836
});
18341837

yarn-project/stdlib/src/checkpoint/checkpoint_data.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ export type ProposedOnlyCheckpointData = {
4242
feeAssetPriceModifier: bigint;
4343
};
4444

45+
/** Inbox consumption record of a proposed checkpoint, mirroring what `propose` writes for it on L1. */
46+
export type ProposedInboxConsumption = {
47+
/**
48+
* Cumulative Inbox message count consumed as of this checkpoint: its last block's L1-to-L2 message tree leaf count
49+
* under compact indexing (AZIP-22 Fast Inbox). L1 records this per checkpoint and reads it back as the parent total
50+
* when validating the next checkpoint's consumption, so a simulation against a not-yet-mined parent must override
51+
* the parent's cell with it — otherwise the parent reads as having consumed nothing and the child's consumption
52+
* looks larger than it is.
53+
*/
54+
inboxMsgTotal: bigint;
55+
};
56+
4557
/** Lightweight checkpoint metadata without full block data. */
4658
export type CheckpointData = CommonCheckpointData & StorageEnrichedCheckpointData & L1EnrichedCheckpointData;
4759

@@ -51,7 +63,7 @@ export type ProposedCheckpointInput = CommonCheckpointData & ProposedOnlyCheckpo
5163

5264
/** Full data for a proposed checkpoint (proposed but not yet L1-confirmed).
5365
* Includes fee-relevant fields used during pipelining to compute the fee header override. */
54-
export type ProposedCheckpointData = ProposedCheckpointInput & StorageEnrichedCheckpointData;
66+
export type ProposedCheckpointData = ProposedCheckpointInput & StorageEnrichedCheckpointData & ProposedInboxConsumption;
5567

5668
export const ProposedCheckpointDataSchema = z.object({
5769
checkpointNumber: CheckpointNumberSchema,
@@ -62,6 +74,7 @@ export const ProposedCheckpointDataSchema = z.object({
6274
blockCount: z.number(),
6375
totalManaUsed: schemas.BigInt,
6476
feeAssetPriceModifier: schemas.BigInt,
77+
inboxMsgTotal: schemas.BigInt,
6578
});
6679

6780
export const CheckpointDataSchema = z

yarn-project/stdlib/src/checkpoint/previous_checkpoint_out_hashes.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function proposedParent(number: number, slot: number, outHash: Fr): ProposedChec
3737
blockCount: 1,
3838
totalManaUsed: 0n,
3939
feeAssetPriceModifier: 0n,
40+
inboxMsgTotal: 0n,
4041
};
4142
}
4243

yarn-project/stdlib/src/checkpoint/simulation_overrides.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('computePipelinedParentFeeHeader', () => {
3131
blockCount: 1,
3232
totalManaUsed: 5000n,
3333
feeAssetPriceModifier: 100n,
34+
inboxMsgTotal: 0n,
3435
};
3536

3637
const grandparentFeeHeader: FeeHeader = {
@@ -157,6 +158,7 @@ describe('buildCheckpointSimulationOverridesPlan', () => {
157158
blockCount: 1,
158159
totalManaUsed: 5000n,
159160
feeAssetPriceModifier: 100n,
161+
inboxMsgTotal: 1500n,
160162
};
161163
}
162164

@@ -194,6 +196,8 @@ describe('buildCheckpointSimulationOverridesPlan', () => {
194196
expect(plan?.pendingCheckpointState?.outHash).toEqual(proposedData.checkpointOutHash);
195197
expect(plan?.pendingCheckpointState?.payloadDigest).toBeDefined();
196198
expect(plan?.pendingCheckpointState?.feeHeader).toBeDefined();
199+
// Without this the parent reads as having consumed no Inbox messages, inflating the child's consumed count.
200+
expect(plan?.pendingCheckpointState?.inboxMsgTotal).toEqual(proposedData.inboxMsgTotal);
197201
});
198202

199203
it('throws when the pipelined parent does not match the expected parent checkpoint', async () => {

yarn-project/stdlib/src/checkpoint/simulation_overrides.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,25 @@ export async function buildCheckpointSimulationOverridesPlan(
7373
builder.withChainTips({ pending: overridenChainTip, proven: overridenChainTip });
7474

7575
if (input.proposedCheckpointData) {
76-
const { header, archive, checkpointOutHash, feeAssetPriceModifier } = input.proposedCheckpointData;
76+
const { header, archive, checkpointOutHash, feeAssetPriceModifier, inboxMsgTotal } = input.proposedCheckpointData;
7777
builder.withPendingArchive(archive.root);
7878
// Override every locally-derivable `tempCheckpointLogs[parent]` field that L1 will eventually
79-
// write. `slotNumber` is load-bearing for `STFLib.canPruneAtTime`: without it the cell reads
79+
// write. `slotNumber` is required by `STFLib.canPruneAtTime`: without it the cell reads
8080
// slotNumber 0, the contract treats the pending tip as belonging to an expired epoch, and
8181
// `getEffectivePendingCheckpointNumber` silently collapses pending back to proven — producing
82-
// a spurious `Rollup__InvalidArchive` against the on-chain genesis archive. The other fields
83-
// (headerHash, outHash, payloadDigest) are not strictly load-bearing for `canProposeAt` /
84-
// `validateBlockHeader`, but mirroring the full cell keeps the simulation byte-faithful with
85-
// what the actual `propose()` send will observe, which is a defense against future reads
86-
// taking dependencies on them.
82+
// a spurious `Rollup__InvalidArchive` against the on-chain genesis archive. `inboxMsgTotal` is
83+
// read by `ProposeLib.validateInboxConsumption` as the parent's consumed total: left at zero it
84+
// makes our consumption look like the Inbox's entire history and trips
85+
// `Rollup__TooManyInboxMessagesConsumed`. The remaining fields (headerHash, outHash,
86+
// payloadDigest) are not read by `canProposeAt` / `validateBlockHeader`, but mirroring the full
87+
// cell keeps the simulation byte-faithful with what the actual `propose()` send will observe,
88+
// which is a defense against future reads taking dependencies on them. The parent's
89+
// `inboxConsumedBucket` shares the word and stays zero: no contract path reads it back.
8790
builder.withPendingTempCheckpointLogFields({
8891
headerHash: header.hash(),
8992
outHash: checkpointOutHash,
9093
slotNumber: header.slotNumber,
94+
inboxMsgTotal,
9195
payloadDigest: computeCheckpointPayloadDigest({
9296
header,
9397
archiveRoot: archive.root,

0 commit comments

Comments
 (0)