@@ -9,27 +9,26 @@ import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers';
99
1010import { mock } from 'jest-mock-extended' ;
1111
12- import { type CheckpointProverFactory , CheckpointStore } from './checkpoint-store.js' ;
12+ import { CheckpointStore } from './checkpoint-store.js' ;
1313import type { CheckpointProver } from './job/checkpoint-prover.js' ;
1414
1515describe ( 'CheckpointStore' , ( ) => {
16- let store : TestCheckpointStore ;
17- let blockSource : ReturnType < typeof mock < Pick < L2BlockSource , 'getSyncedL2SlotNumber' | ' getL1Constants'> > > ;
16+ let store : CheckpointStore ;
17+ let blockSource : ReturnType < typeof mock < Pick < L2BlockSource , 'getL1Constants' > > > ;
1818 /** Track stub provers we hand back from the factory. */
1919 const stubs : StubProver [ ] = [ ] ;
2020
2121 // Single-slot epochs make every checkpoint live in its own epoch and slot range.
2222 const l1Constants = { ...EmptyL1RollupConstants , epochDuration : 1 } ;
2323
2424 beforeEach ( ( ) => {
25- blockSource = mock < Pick < L2BlockSource , 'getSyncedL2SlotNumber' | ' getL1Constants'> > ( ) ;
25+ blockSource = mock < Pick < L2BlockSource , 'getL1Constants' > > ( ) ;
2626 blockSource . getL1Constants . mockResolvedValue ( l1Constants ) ;
2727 stubs . length = 0 ;
28- store = new TestCheckpointStore (
28+ store = new CheckpointStore (
2929 blockSource ,
3030 // The deps are not exercised — the factory below ignores them.
3131 { } as any ,
32- { slotWatcherPollIntervalMs : 100 } ,
3332 undefined ,
3433 ( args , _deps ) => {
3534 const stub = makeStubProver ( args . checkpoint , args . epochNumber ) ;
@@ -51,21 +50,35 @@ describe('CheckpointStore', () => {
5150 expect ( stubs . length ) . toBe ( 1 ) ;
5251 } ) ;
5352
54- it ( 'addOrUpdate is idempotent for the same content key (re-add after prune)' , async ( ) => {
53+ it ( 'addOrUpdate reuses the prover for a still-canonical duplicate registration' , async ( ) => {
54+ // An at-least-once re-registration of a checkpoint that has NOT been pruned reuses the running
55+ // prover rather than rebuilding its in-flight work.
5556 const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 } ) ;
5657
5758 const first = await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
58- expect ( first . isPruned ( ) ) . toBe ( false ) ;
59- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
60- expect ( first . isPruned ( ) ) . toBe ( true ) ;
61-
62- // Re-adding the identical checkpoint (same archive root) reuses the existing prover.
6359 const second = await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
6460 expect ( second ) . toBe ( first ) ;
65- expect ( second . isPruned ( ) ) . toBe ( false ) ;
6661 expect ( stubs . length ) . toBe ( 1 ) ;
6762 } ) ;
6863
64+ it ( 'addOrUpdate rebuilds a fresh prover for a re-add after prune' , async ( ) => {
65+ const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 } ) ;
66+
67+ const first = await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
68+ expect ( first . isCancelled ( ) ) . toBe ( false ) ;
69+ store . cancelAndRemoveAboveBlock ( BlockNumber ( 0 ) ) ;
70+ // The pruned prover is cancelled and dropped from the store.
71+ expect ( first . isCancelled ( ) ) . toBe ( true ) ;
72+ expect ( store . getByCheckpoint ( cp ) ) . toBeUndefined ( ) ;
73+
74+ // Re-adding the identical checkpoint (same archive root) constructs a fresh prover — the pruned
75+ // one's forked world-state reads did not survive, so there is nothing to reuse.
76+ const second = await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
77+ expect ( second ) . not . toBe ( first ) ;
78+ expect ( second . isCancelled ( ) ) . toBe ( false ) ;
79+ expect ( stubs . length ) . toBe ( 2 ) ;
80+ } ) ;
81+
6982 it ( 'addOrUpdate refuses a conflicting canonical checkpoint at the same slot' , async ( ) => {
7083 // Two canonical checkpoints sharing a slot would be a parallel chain. The store rejects
7184 // the second; the caller must prune the first (via the chain-pruned event) before the
@@ -79,17 +92,17 @@ describe('CheckpointStore', () => {
7992 / c a n o n i c a l c h e c k p o i n t a l r e a d y o c c u p i e s t h i s s l o t / i,
8093 ) ;
8194
82- // After the predecessor is pruned, the replacement is accepted and keys to a distinct
83- // prover (different archive root → different content id).
84- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
85- expect ( proverA . isPruned ( ) ) . toBe ( true ) ;
95+ // After the predecessor is pruned (cancelled and removed) , the replacement is accepted and keys
96+ // to a distinct prover (different archive root → different content id).
97+ store . cancelAndRemoveAboveBlock ( BlockNumber ( 0 ) ) ;
98+ expect ( proverA . isCancelled ( ) ) . toBe ( true ) ;
8699 const proverB = await store . addOrUpdate ( b , makeRegisterData ( ) ) ;
87100 expect ( proverB ) . not . toBe ( proverA ) ;
88- expect ( proverB . isPruned ( ) ) . toBe ( false ) ;
101+ expect ( proverB . isCancelled ( ) ) . toBe ( false ) ;
89102 expect ( stubs . length ) . toBe ( 2 ) ;
90103 } ) ;
91104
92- it ( 'markPrunedAboveBlock marks every prover holding a block above the target and returns them' , async ( ) => {
105+ it ( 'cancelAndRemoveAboveBlock cancels and removes every prover holding a block above the target and returns them' , async ( ) => {
93106 // Four single-block checkpoints occupying blocks 1..4 (one block each). Pruning to block 2 orphans the
94107 // checkpoints whose last block is above 2 — checkpoints 3 and 4 — and leaves 1 and 2 canonical.
95108 const cps = await timesAsync ( 4 , i =>
@@ -102,23 +115,26 @@ describe('CheckpointStore', () => {
102115 for ( const cp of cps ) {
103116 await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
104117 }
105- const affected = store . markPrunedAboveBlock ( BlockNumber ( 2 ) ) ;
118+ const affected = store . cancelAndRemoveAboveBlock ( BlockNumber ( 2 ) ) ;
106119 expect ( affected . map ( p => p . checkpoint . number ) ) . toEqual ( [ 3 , 4 ] ) ;
120+ expect ( affected . every ( p => p . isCancelled ( ) ) ) . toBe ( true ) ;
121+ // The orphaned provers are gone from the store; only 1 and 2 remain.
107122 expect ( store . listCanonical ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 , 2 ] ) ;
108123 } ) ;
109124
110- it ( 'markPrunedAboveBlock marks a checkpoint whose block range straddles the target (partially orphaned)' , async ( ) => {
125+ it ( 'cancelAndRemoveAboveBlock removes a checkpoint whose block range straddles the target (partially orphaned)' , async ( ) => {
111126 // A single checkpoint spanning blocks 5..8. A prune to block 6 lands mid-checkpoint: the checkpoint is partially
112- // orphaned (blocks 7, 8 are gone) and must be marked , since its last block (8) is above the target.
127+ // orphaned (blocks 7, 8 are gone) and must be removed , since its last block (8) is above the target.
113128 const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 4 , startBlockNumber : 5 } ) ;
114129 await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
115130
116- const affected = store . markPrunedAboveBlock ( BlockNumber ( 6 ) ) ;
131+ const affected = store . cancelAndRemoveAboveBlock ( BlockNumber ( 6 ) ) ;
117132 expect ( affected . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 ] ) ;
133+ expect ( affected [ 0 ] . isCancelled ( ) ) . toBe ( true ) ;
118134 expect ( store . listCanonical ( ) ) . toEqual ( [ ] ) ;
119135 } ) ;
120136
121- it ( 'reapExpired drops canonical provers whose epoch is ≤ expiredEpoch' , async ( ) => {
137+ it ( 'reapExpired drops provers whose epoch is ≤ expiredEpoch' , async ( ) => {
122138 // With epochDuration=1 each checkpoint's slot is also its epoch number.
123139 const cps = await Promise . all ( [
124140 Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ,
@@ -133,74 +149,7 @@ describe('CheckpointStore', () => {
133149 expect ( remainingNumbers ) . toEqual ( [ 3 ] ) ;
134150 } ) ;
135151
136- it ( 'reapExpired leaves pruned provers in place' , async ( ) => {
137- const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ;
138- await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
139- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
140- store . reapExpired ( EpochNumber ( 10 ) ) ;
141- expect ( store . listAll ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 ] ) ;
142- } ) ;
143-
144- // ---------------- slot watcher ----------------
145-
146- it ( 'slot watcher reaps pruned provers whose slot is strictly before the synced slot' , async ( ) => {
147- const cp1 = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ;
148- const cp2 = await Checkpoint . random ( CheckpointNumber ( 2 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 2 ) } ) ;
149- const cp3 = await Checkpoint . random ( CheckpointNumber ( 3 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 3 ) } ) ;
150- for ( const cp of [ cp1 , cp2 , cp3 ] ) {
151- await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
152- }
153- // Prune everything above checkpoint 0 ⇒ all three flip to pruned.
154- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
155- blockSource . getSyncedL2SlotNumber . mockResolvedValue ( SlotNumber ( 3 ) ) ;
156-
157- await store . triggerSlotWatcherTick ( ) ;
158-
159- // Slots 1 and 2 are < 3 and get reaped; slot 3 is not strictly less, so it stays.
160- expect ( store . listAll ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 3 ] ) ;
161- // Reaped stubs were cancelled by the watcher.
162- expect ( stubs . find ( s => s . checkpoint . number === 1 ) ! . cancelled ) . toBe ( true ) ;
163- expect ( stubs . find ( s => s . checkpoint . number === 2 ) ! . cancelled ) . toBe ( true ) ;
164- expect ( stubs . find ( s => s . checkpoint . number === 3 ) ! . cancelled ) . toBe ( false ) ;
165- } ) ;
166-
167- it ( 'slot watcher leaves canonical provers in place even when their slot is past the synced slot' , async ( ) => {
168- // Canonical provers must survive — only pruned provers are eligible for reaping.
169- const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ;
170- await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
171- blockSource . getSyncedL2SlotNumber . mockResolvedValue ( SlotNumber ( 10 ) ) ;
172-
173- await store . triggerSlotWatcherTick ( ) ;
174-
175- expect ( store . listAll ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 ] ) ;
176- expect ( stubs [ 0 ] . cancelled ) . toBe ( false ) ;
177- } ) ;
178-
179- it ( 'slot watcher no-ops when getSyncedL2SlotNumber returns undefined' , async ( ) => {
180- const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ;
181- await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
182- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
183- blockSource . getSyncedL2SlotNumber . mockResolvedValue ( undefined ) ;
184-
185- await store . triggerSlotWatcherTick ( ) ;
186-
187- // No synced slot yet ⇒ watcher doesn't know whether the chain has moved past, so it
188- // keeps the pruned prover around for a possible re-add.
189- expect ( store . listAll ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 ] ) ;
190- expect ( stubs [ 0 ] . cancelled ) . toBe ( false ) ;
191- } ) ;
192-
193- it ( 'slot watcher swallows getSyncedL2SlotNumber errors instead of crashing the tick' , async ( ) => {
194- const cp = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 1 ) } ) ;
195- await store . addOrUpdate ( cp , makeRegisterData ( ) ) ;
196- store . markPrunedAboveBlock ( BlockNumber ( 0 ) ) ;
197- blockSource . getSyncedL2SlotNumber . mockRejectedValue ( new Error ( 'archiver unavailable' ) ) ;
198-
199- await expect ( store . triggerSlotWatcherTick ( ) ) . resolves . toBeUndefined ( ) ;
200- expect ( store . listAll ( ) . map ( p => p . checkpoint . number ) ) . toEqual ( [ 1 ] ) ;
201- } ) ;
202-
203- it ( 'listCanonicalForEpoch returns only canonical provers in the epoch slot range' , async ( ) => {
152+ it ( 'listCanonicalForEpoch returns only provers in the epoch slot range' , async ( ) => {
204153 // With epochDuration=1, each epoch's slot range is exactly [slot, slot].
205154 const cp1 = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 10 ) } ) ;
206155 const cp2 = await Checkpoint . random ( CheckpointNumber ( 2 ) , { numBlocks : 1 , slotNumber : SlotNumber ( 11 ) } ) ;
@@ -220,12 +169,8 @@ type StubProver = {
220169 checkpoint : Checkpoint ;
221170 slotNumber : SlotNumber ;
222171 epochNumber : EpochNumber ;
223- pruned : boolean ;
224172 cancelled : boolean ;
225- isPruned ( ) : boolean ;
226173 isCancelled ( ) : boolean ;
227- markPruned ( ) : void ;
228- markCanonical ( ) : void ;
229174 cancel ( opts ?: { routine ?: boolean } ) : void ;
230175 whenDone ( ) : Promise < void > ;
231176} ;
@@ -237,20 +182,10 @@ function makeStubProver(checkpoint: Checkpoint, epochNumber: EpochNumber): StubP
237182 checkpoint,
238183 slotNumber : checkpoint . header . slotNumber ,
239184 epochNumber,
240- pruned : false ,
241185 cancelled : false ,
242- isPruned ( ) {
243- return this . pruned ;
244- } ,
245186 isCancelled ( ) {
246187 return this . cancelled ;
247188 } ,
248- markPruned ( ) {
249- this . pruned = true ;
250- } ,
251- markCanonical ( ) {
252- this . pruned = false ;
253- } ,
254189 cancel ( ) {
255190 this . cancelled = true ;
256191 } ,
@@ -268,24 +203,3 @@ function makeRegisterData() {
268203 previousArchiveSiblingPath : makeTuple ( ARCHIVE_HEIGHT , ( ) => Fr . ZERO ) ,
269204 } ;
270205}
271-
272- /**
273- * Subclass that exposes the protected `reapPrunedPastSlot` so tests can drive a single
274- * SlotWatcher tick directly — avoids spinning up the underlying `RunningPromise` and
275- * waiting on its polling interval.
276- */
277- class TestCheckpointStore extends CheckpointStore {
278- constructor (
279- blockSource : ConstructorParameters < typeof CheckpointStore > [ 0 ] ,
280- proverDeps : ConstructorParameters < typeof CheckpointStore > [ 1 ] ,
281- options : ConstructorParameters < typeof CheckpointStore > [ 2 ] ,
282- bindings : ConstructorParameters < typeof CheckpointStore > [ 3 ] ,
283- factory : CheckpointProverFactory ,
284- ) {
285- super ( blockSource , proverDeps , options , bindings , factory ) ;
286- }
287-
288- public triggerSlotWatcherTick ( ) : Promise < void > {
289- return this . reapPrunedPastSlot ( ) ;
290- }
291- }
0 commit comments