11import { BlockNumber , CheckpointNumber } from '@aztec/foundation/branded-types' ;
22import { timesParallel } from '@aztec/foundation/collection' ;
33import { Fr } from '@aztec/foundation/curves/bn254' ;
4+ import type { AztecAsyncKVStore } from '@aztec/kv-store' ;
45import { openTmpStore } from '@aztec/kv-store/lmdb-v2' ;
56import { L2TipsKVStore } from '@aztec/kv-store/stores' ;
67import { GENESIS_CHECKPOINT_HEADER_HASH , L2Block , L2BlockHash , type L2BlockStream } from '@aztec/stdlib/block' ;
8+ import { Checkpoint , L1PublishedData , PublishedCheckpoint } from '@aztec/stdlib/checkpoint' ;
79import type { AztecNode } from '@aztec/stdlib/interfaces/client' ;
810
911import { jest } from '@jest/globals' ;
1012import { type MockProxy , mock } from 'jest-mock-extended' ;
1113
14+ import type { BlockSynchronizerConfig } from '../config/index.js' ;
1215import { AnchorBlockStore } from '../storage/anchor_block_store/anchor_block_store.js' ;
1316import { NoteStore } from '../storage/note_store/note_store.js' ;
1417import { PrivateEventStore } from '../storage/private_event_store/private_event_store.js' ;
1518import { BlockSynchronizer } from './block_synchronizer.js' ;
1619
1720describe ( 'BlockSynchronizer' , ( ) => {
1821 let synchronizer : BlockSynchronizer ;
22+ let store : AztecAsyncKVStore ;
1923 let tipsStore : L2TipsKVStore ;
2024 let anchorBlockStore : AnchorBlockStore ;
2125 let noteStore : NoteStore ;
@@ -29,15 +33,19 @@ describe('BlockSynchronizer', () => {
2933 }
3034 } ;
3135
36+ const createSynchronizer = ( config : Partial < BlockSynchronizerConfig > = { } ) => {
37+ return new TestSynchronizer ( aztecNode , store , anchorBlockStore , noteStore , privateEventStore , tipsStore , config ) ;
38+ } ;
39+
3240 beforeEach ( async ( ) => {
33- const store = await openTmpStore ( 'test' ) ;
41+ store = await openTmpStore ( 'test' ) ;
3442 blockStream = mock < L2BlockStream > ( ) ;
3543 aztecNode = mock < AztecNode > ( ) ;
3644 tipsStore = new L2TipsKVStore ( store , 'pxe' ) ;
3745 anchorBlockStore = new AnchorBlockStore ( store ) ;
3846 noteStore = new NoteStore ( store ) ;
3947 privateEventStore = new PrivateEventStore ( store ) ;
40- synchronizer = new TestSynchronizer ( aztecNode , store , anchorBlockStore , noteStore , privateEventStore , tipsStore ) ;
48+ synchronizer = createSynchronizer ( ) ;
4149 } ) ;
4250
4351 it ( 'sets header from latest block' , async ( ) => {
@@ -95,4 +103,140 @@ describe('BlockSynchronizer', () => {
95103
96104 expect ( rollback ) . toHaveBeenCalledWith ( 3 , 4 ) ;
97105 } ) ;
106+
107+ describe ( 'syncChainTip config' , ( ) => {
108+ it ( 'updates anchor on blocks-added when syncChainTip is proposed (default)' , async ( ) => {
109+ synchronizer = createSynchronizer ( { syncChainTip : 'proposed' } ) ;
110+ const block = await L2Block . random ( BlockNumber ( 1 ) ) ;
111+ await synchronizer . handleBlockStreamEvent ( { type : 'blocks-added' , blocks : [ block ] } ) ;
112+
113+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
114+ expect ( obtainedHeader . equals ( block . header ) ) . toBe ( true ) ;
115+ } ) ;
116+
117+ it ( 'does not update anchor on blocks-added when syncChainTip is checkpointed' , async ( ) => {
118+ synchronizer = createSynchronizer ( { syncChainTip : 'checkpointed' } ) ;
119+
120+ // First set a known anchor
121+ const initialBlock = await L2Block . random ( BlockNumber ( 0 ) ) ;
122+ await anchorBlockStore . setHeader ( initialBlock . header ) ;
123+
124+ // blocks-added should NOT update the anchor
125+ const newBlock = await L2Block . random ( BlockNumber ( 1 ) ) ;
126+ await synchronizer . handleBlockStreamEvent ( { type : 'blocks-added' , blocks : [ newBlock ] } ) ;
127+
128+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
129+ expect ( obtainedHeader . equals ( initialBlock . header ) ) . toBe ( true ) ;
130+ } ) ;
131+
132+ it ( 'updates anchor on chain-checkpointed when syncChainTip is checkpointed' , async ( ) => {
133+ synchronizer = createSynchronizer ( { syncChainTip : 'checkpointed' } ) ;
134+
135+ // Set initial anchor
136+ const initialBlock = await L2Block . random ( BlockNumber ( 0 ) ) ;
137+ await anchorBlockStore . setHeader ( initialBlock . header ) ;
138+
139+ // Create a checkpoint with a block
140+ const checkpointBlock = await L2Block . random ( BlockNumber ( 1 ) ) ;
141+ const checkpoint = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 } ) ;
142+ // Replace the random block with our known block
143+ checkpoint . blocks [ 0 ] = checkpointBlock ;
144+
145+ const publishedCheckpoint = new PublishedCheckpoint ( checkpoint , L1PublishedData . random ( ) , [ ] ) ;
146+
147+ await synchronizer . handleBlockStreamEvent ( {
148+ type : 'chain-checkpointed' ,
149+ checkpoint : publishedCheckpoint ,
150+ block : { number : BlockNumber ( 1 ) , hash : '0x456' } ,
151+ } ) ;
152+
153+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
154+ expect ( obtainedHeader . equals ( checkpointBlock . header ) ) . toBe ( true ) ;
155+ } ) ;
156+
157+ it ( 'does not update anchor on chain-checkpointed when syncChainTip is proposed' , async ( ) => {
158+ synchronizer = createSynchronizer ( { syncChainTip : 'proposed' } ) ;
159+
160+ // Set initial anchor via blocks-added
161+ const initialBlock = await L2Block . random ( BlockNumber ( 1 ) ) ;
162+ await synchronizer . handleBlockStreamEvent ( { type : 'blocks-added' , blocks : [ initialBlock ] } ) ;
163+
164+ // Create a different checkpoint
165+ const checkpoint = await Checkpoint . random ( CheckpointNumber ( 1 ) , { numBlocks : 1 } ) ;
166+ const publishedCheckpoint = new PublishedCheckpoint ( checkpoint , L1PublishedData . random ( ) , [ ] ) ;
167+
168+ await synchronizer . handleBlockStreamEvent ( {
169+ type : 'chain-checkpointed' ,
170+ checkpoint : publishedCheckpoint ,
171+ block : { number : BlockNumber ( 1 ) , hash : '0x456' } ,
172+ } ) ;
173+
174+ // Anchor should still be the initial block, not the checkpoint block
175+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
176+ expect ( obtainedHeader . equals ( initialBlock . header ) ) . toBe ( true ) ;
177+ } ) ;
178+
179+ it ( 'updates anchor on chain-proven when syncChainTip is proven' , async ( ) => {
180+ synchronizer = createSynchronizer ( { syncChainTip : 'proven' } ) ;
181+
182+ // Set initial anchor
183+ const initialBlock = await L2Block . random ( BlockNumber ( 0 ) ) ;
184+ await anchorBlockStore . setHeader ( initialBlock . header ) ;
185+
186+ // Mock node to return block header
187+ const provenBlock = await L2Block . random ( BlockNumber ( 5 ) ) ;
188+ aztecNode . getBlockHeader . mockResolvedValue ( provenBlock . header ) ;
189+
190+ await synchronizer . handleBlockStreamEvent ( {
191+ type : 'chain-proven' ,
192+ block : { number : BlockNumber ( 5 ) , hash : '0x789' } ,
193+ } ) ;
194+
195+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
196+ expect ( obtainedHeader . equals ( provenBlock . header ) ) . toBe ( true ) ;
197+ } ) ;
198+
199+ it ( 'updates anchor on chain-finalized when syncChainTip is finalized' , async ( ) => {
200+ synchronizer = createSynchronizer ( { syncChainTip : 'finalized' } ) ;
201+
202+ // Set initial anchor
203+ const initialBlock = await L2Block . random ( BlockNumber ( 0 ) ) ;
204+ await anchorBlockStore . setHeader ( initialBlock . header ) ;
205+
206+ // Mock node to return block header
207+ const finalizedBlock = await L2Block . random ( BlockNumber ( 10 ) ) ;
208+ aztecNode . getBlockHeader . mockResolvedValue ( finalizedBlock . header ) ;
209+
210+ await synchronizer . handleBlockStreamEvent ( {
211+ type : 'chain-finalized' ,
212+ block : { number : BlockNumber ( 10 ) , hash : '0xabc' } ,
213+ } ) ;
214+
215+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
216+ expect ( obtainedHeader . equals ( finalizedBlock . header ) ) . toBe ( true ) ;
217+ } ) ;
218+
219+ it ( 'ignores prune event when anchor is already at or below prune point' , async ( ) => {
220+ synchronizer = createSynchronizer ( { syncChainTip : 'checkpointed' } ) ;
221+
222+ // Set anchor to block 2
223+ const anchorBlock = await L2Block . random ( BlockNumber ( 2 ) ) ;
224+ await anchorBlockStore . setHeader ( anchorBlock . header ) ;
225+
226+ const rollback = jest . spyOn ( noteStore , 'rollback' ) . mockImplementation ( ( ) => Promise . resolve ( ) ) ;
227+
228+ // Prune to block 3 (above anchor) - should be ignored
229+ await synchronizer . handleBlockStreamEvent ( {
230+ type : 'chain-pruned' ,
231+ block : { number : BlockNumber ( 3 ) , hash : '0x3' } ,
232+ checkpoint : { number : CheckpointNumber . ZERO , hash : GENESIS_CHECKPOINT_HEADER_HASH . toString ( ) } ,
233+ } ) ;
234+
235+ expect ( rollback ) . not . toHaveBeenCalled ( ) ;
236+
237+ // Anchor should be unchanged
238+ const obtainedHeader = await anchorBlockStore . getBlockHeader ( ) ;
239+ expect ( obtainedHeader . equals ( anchorBlock . header ) ) . toBe ( true ) ;
240+ } ) ;
241+ } ) ;
98242} ) ;
0 commit comments