Skip to content

Commit e3ddeed

Browse files
committed
fix(sequencer): override pending-tip slotNumber so canPruneAtTime can't bypass pending override
When pipelining with a proposed parent, the canProposeAt simulation overrides `tips.pending` to the parent's checkpoint number but the centralised override helper was not setting `tempCheckpointLogs[pending].slotNumber`. The L1 `STFLib.canPruneAtTime(_ts)` reads that slot via `getEpochForCheckpoint(proven + 1)`; with the override unset it falls back to 0 (epoch 0), which at any non-trivial simulation timestamp is far past its proof-submission window. `canPruneAtTime` returns true, `getEffectivePendingCheckpointNumber` collapses pending back to proven, the pending override is silently bypassed, and `canProposeAtTime` then compares the supplied archive against `archives[proven]` (= genesis on a fresh chain), reverting with `Rollup__InvalidArchive(genesis, syncedTo.archive)`. This was the cause of the "Rollup contract check failed" / proposer-rollup-check-failed reverts across the e2e_epochs_mbps, epochs_missed_l1_slot, and e2e_l1_publisher tests after 7c9268f centralised the override helper and dropped the `withPendingTempCheckpointLogFields` call the pre-refactor helper had. Restore the slotNumber override (only) in the centralised helper. The other fields the pre-refactor helper set (headerHash, outHash, payloadDigest) were only consumed by the propose-call simulation that 85b6a02 removed; the post-refactor enqueue-time simulations (canProposeAt and validateBlockHeader with ignoreSignatures: true) do not read them. To make the partial override expressible, loosen `withPendingTempCheckpointLogFields` to accept any subset of fields — the underlying `makeTempCheckpointLogOverride` already emits a stateDiff entry per field actually set. Also switch `archiveForCheck` and `lastArchiveRoot` from `syncedTo.archive` to `syncedTo.proposedCheckpointData.archive.root` when pipelining with a proposed parent. The two values agree in steady state but can transiently diverge when world-state has not yet applied the proposed parent's blocks locally; the proposed-checkpoint archive is the value L1 will actually see at archives[pending] once the parent lands, and using it on both sides of the comparison keeps the override and the supplied archive in lockstep. Diagnosed against /tmp/a1442bdbf1cf22fe.log (epochs_mbps) and /tmp/b84862271b974e83.log (e2e_l1_publisher); confirmed independently by codex.
1 parent 28d32d6 commit e3ddeed

3 files changed

Lines changed: 34 additions & 8 deletions

File tree

yarn-project/ethereum/src/contracts/chain_state_override.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,21 @@ export class SimulationOverridesBuilder {
8787
}
8888

8989
/**
90-
* Overrides the locally-derivable `tempCheckpointLogs` cell fields for the configured pending
91-
* checkpoint. Callers populate these together because they all come from the same proposed
92-
* checkpoint payload — there is no use case for setting them independently.
90+
* Overrides one or more `tempCheckpointLogs` cell fields for the configured pending checkpoint.
91+
* Fields are independent: any subset can be provided. The translator (`makeTempCheckpointLogOverride`)
92+
* emits a stateDiff entry per field actually set, so unspecified fields stay at their on-chain
93+
* values.
94+
*
95+
* `slotNumber` is load-bearing for `STFLib.canPruneAtTime`: when the simulation overrides `pending`
96+
* to a checkpoint that has no on-chain `tempCheckpointLogs` entry yet, the missing slotNumber falls
97+
* back to 0 and the contract treats the pending tip as belonging to epoch 0, triggering a phantom
98+
* prune that silently undoes the `pending` override.
9399
*/
94100
public withPendingTempCheckpointLogFields(fields: {
95-
headerHash: Fr;
96-
outHash: Fr;
97-
payloadDigest: Buffer32;
98-
slotNumber: SlotNumber;
101+
headerHash?: Fr;
102+
outHash?: Fr;
103+
payloadDigest?: Buffer32;
104+
slotNumber?: SlotNumber;
99105
}): this {
100106
this.assertPendingCheckpointNumber();
101107
this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), ...fields };

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ export async function buildCheckpointSimulationOverridesPlan(
4444
if (input.lastArchiveRoot !== undefined) {
4545
builder.withPendingArchive(input.lastArchiveRoot);
4646
}
47+
// When pipelining with a proposed parent we must also override the parent's
48+
// tempCheckpointLogs.slotNumber: without it `STFLib.canPruneAtTime` reads a slotNumber of 0
49+
// for the overridden pending tip, decides the tip is in a long-expired epoch, and reports the
50+
// chain as prunable. `getEffectivePendingCheckpointNumber` then collapses pending back to
51+
// proven and the pending override is silently bypassed, producing a spurious
52+
// `Rollup__InvalidArchive` against the on-chain genesis archive. The invalidate-only override
53+
// path does not have a proposed parent to read the slot from; it relies on the absence of an
54+
// unproven pending tip to avoid this code path.
55+
if (input.proposedCheckpointData) {
56+
builder.withPendingTempCheckpointLogFields({
57+
slotNumber: input.proposedCheckpointData.header.slotNumber,
58+
});
59+
}
4760
}
4861
if (feeHeader) {
4962
builder.withPendingFeeHeader(feeHeader);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
393393
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber}) for target slot ${targetSlot}`,
394394
{ targetSlot, parentCheckpointNumber: CheckpointNumber(checkpointNumber - 1) },
395395
);
396+
// Match what L1 will see at archives[pending] once the proposed parent lands: the parent's
397+
// own archive root from the gossiped proposal. `syncedTo.archive` is the world-state-local
398+
// view and can transiently diverge from the proposed parent (e.g. before the proposed
399+
// parent's blocks have been applied locally); diverging here would cause the canProposeAt
400+
// override to set archives[pending] to one value while we present another for comparison.
401+
archiveForCheck = syncedTo.proposedCheckpointData!.archive.root;
396402
// Clear the invalidation - the proposed checkpoint should handle it.
397403
invalidateCheckpoint = undefined;
398404
} else if (invalidateCheckpoint) {
@@ -410,7 +416,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
410416
proposedCheckpointData:
411417
isPipelining && syncedTo.hasProposedCheckpoint ? syncedTo.proposedCheckpointData : undefined,
412418
invalidateToPendingCheckpointNumber: invalidateCheckpoint?.forcePendingCheckpointNumber,
413-
lastArchiveRoot: isPipelining && syncedTo.hasProposedCheckpoint ? syncedTo.archive : undefined,
419+
lastArchiveRoot:
420+
isPipelining && syncedTo.hasProposedCheckpoint ? syncedTo.proposedCheckpointData!.archive.root : undefined,
414421
rollup: this.rollupContract,
415422
log: this.log,
416423
});

0 commit comments

Comments
 (0)