@@ -29,6 +29,7 @@ The Based Sequencer implements the `Sequencer` interface from `core/sequencer.go
2929Transactions are retrieved from DA in ** epochs** , not individual DA blocks. An epoch is a range of DA blocks defined by ` DAEpochForcedInclusion ` in the genesis configuration.
3030
3131** Example** : If ` DAStartHeight = 100 ` and ` DAEpochForcedInclusion = 10 ` :
32+
3233- Epoch 1: DA heights 100-109
3334- Epoch 2: DA heights 110-119
3435- Epoch 3: DA heights 120-129
@@ -77,7 +78,7 @@ The checkpoint system tracks the exact position in the transaction stream to ena
7778type Checkpoint struct {
7879 // DAHeight is the DA block height currently being processed
7980 DAHeight uint64
80-
81+
8182 // TxIndex is the index of the next transaction to process
8283 // within the DA block's forced inclusion batch
8384 TxIndex uint64
@@ -87,21 +88,26 @@ type Checkpoint struct {
8788### How Checkpoints Work
8889
8990#### 1. Initial State
91+
9092```
9193Checkpoint: (DAHeight: 100, TxIndex: 0)
9294- Ready to fetch epoch starting at DA height 100
9395```
9496
9597#### 2. Fetching Transactions
98+
9699When ` GetNextBatch() ` is called and we're at an epoch end:
100+
97101```
98102Request: GetNextBatch(maxBytes: 1MB)
99103Action: Fetch all transactions from epoch (DA heights 100-109)
100104Result: currentBatchTxs = [tx1, tx2, tx3, ..., txN] (from entire epoch)
101105```
102106
103107#### 3. Processing Transactions
108+
104109Transactions are processed incrementally, respecting ` maxBytes ` :
110+
105111```
106112Batch 1: [tx1, tx2] (fits in maxBytes)
107113Checkpoint: (DAHeight: 100, TxIndex: 2)
@@ -122,15 +128,15 @@ Checkpoint: (DAHeight: 101, TxIndex: 0)
122128``` go
123129if txCount > 0 {
124130 s.checkpoint .TxIndex += txCount
125-
131+
126132 // Move to next DA height when current one is exhausted
127133 if s.checkpoint .TxIndex >= uint64 (len (s.currentBatchTxs )) {
128134 s.checkpoint .DAHeight ++
129135 s.checkpoint .TxIndex = 0
130136 s.currentBatchTxs = nil
131137 s.SetDAHeight (s.checkpoint .DAHeight )
132138 }
133-
139+
134140 // Persist checkpoint to disk
135141 if err := s.checkpointStore .Save (ctx, s.checkpoint ); err != nil {
136142 return nil , fmt.Errorf (" failed to save checkpoint: % w" , err)
@@ -143,6 +149,7 @@ if txCount > 0 {
143149#### Scenario: Crash Mid-Epoch
144150
145151** Setup** :
152+
146153- Epoch 1 spans DA heights 100-109
147154- At DA height 109, fetched all transactions from the epoch
148155- Processed transactions up to DA height 105, TxIndex 3
@@ -164,16 +171,19 @@ if txCount > 0 {
164171** The entire epoch will be re-fetched after a crash** , even with fine-grained checkpoints.
165172
166173** Why?**
174+
167175- Transactions are only available at epoch boundaries
168176- In-memory cache (` currentBatchTxs ` ) is lost on restart
169177- Must wait until the next epoch end to fetch transactions again
170178
171179** What the checkpoint prevents** :
180+
172181- ✅ Re-execution of already processed transactions
173182- ✅ Correct resumption within a DA block's transaction list
174183- ✅ No transaction loss or duplication
175184
176185** What the checkpoint does NOT prevent** :
186+
177187- ❌ Re-fetching the entire epoch from DA
178188- ❌ Re-validation of previously fetched transactions
179189
@@ -191,291 +201,4 @@ err := checkpointStore.Save(ctx, checkpoint) // Save to disk
191201err := checkpointStore.Delete (ctx) // Delete from disk
192202```
193203
194- The checkpoint is serialized using Protocol Buffers (` pb.BasedCheckpoint ` ) for efficient storage and cross-version compatibility.
195-
196- ## Transaction Processing Flow
197-
198- ### Full Flow Diagram
199-
200- ```
201- ┌─────────────────────────────────────────────────────────────┐
202- │ 1. GetNextBatch() called │
203- └──────────────────────┬──────────────────────────────────────┘
204- │
205- ▼
206- ┌─────────────────────────────────────────────────────────────┐
207- │ 2. Check: Do we have cached transactions? │
208- │ - currentBatchTxs empty OR all consumed? │
209- └──────────────────────┬──────────────────────────────────────┘
210- │ YES
211- ▼
212- ┌─────────────────────────────────────────────────────────────┐
213- │ 3. fetchNextDABatch(checkpoint.DAHeight) │
214- │ - Calls RetrieveForcedIncludedTxs() │
215- └──────────────────────┬──────────────────────────────────────┘
216- │
217- ▼
218- ┌─────────────────────────────────────────────────────────────┐
219- │ 4. Is current DAHeight an epoch end? │
220- └──────┬──────────────────────────────────────┬───────────────┘
221- │ NO │ YES
222- ▼ ▼
223- ┌──────────────────┐ ┌──────────────────────────┐
224- │ Return empty │ │ Fetch entire epoch from │
225- │ transactions │ │ DA (all heights in epoch)│
226- └──────┬───────────┘ └──────────┬───────────────┘
227- │ │
228- │ ▼
229- │ ┌──────────────────────────┐
230- │ │ Validate blob sizes │
231- │ │ Cache in currentBatchTxs │
232- │ └──────────┬───────────────┘
233- │ │
234- └─────────────────┬───────────────────┘
235- ▼
236- ┌─────────────────────────────────────────────────────────────┐
237- │ 5. createBatchFromCheckpoint(maxBytes) │
238- │ - Start from checkpoint.TxIndex │
239- │ - Add transactions until maxBytes reached │
240- │ - Mark all as force-included │
241- └──────────────────────┬──────────────────────────────────────┘
242- │
243- ▼
244- ┌─────────────────────────────────────────────────────────────┐
245- │ 6. Update checkpoint │
246- │ - checkpoint.TxIndex += len(batch.Transactions) │
247- │ - If consumed all txs from current DA height: │
248- │ * checkpoint.DAHeight++ │
249- │ * checkpoint.TxIndex = 0 │
250- │ * Clear currentBatchTxs cache │
251- └──────────────────────┬──────────────────────────────────────┘
252- │
253- ▼
254- ┌─────────────────────────────────────────────────────────────┐
255- │ 7. Persist checkpoint to disk │
256- └──────────────────────┬──────────────────────────────────────┘
257- │
258- ▼
259- ┌─────────────────────────────────────────────────────────────┐
260- │ 8. Return batch to executor for processing │
261- └─────────────────────────────────────────────────────────────┘
262- ```
263-
264- ### Error Handling
265-
266- ** DA Height from Future** :
267- ``` go
268- if errors.Is (err, coreda.ErrHeightFromFuture ) {
269- // Stay at current position
270- // Will retry on next call
271- s.logger .Debug ().Msg (" DA height from future, waiting for DA to produce block" )
272- return nil
273- }
274- ```
275-
276- ** Forced Inclusion Not Configured** :
277- ``` go
278- if errors.Is (err, block.ErrForceInclusionNotConfigured ) {
279- return errors.New (" forced inclusion not configured" )
280- }
281- ```
282-
283- ** Invalid Blob Size** :
284- ``` go
285- if !seqcommon.ValidateBlobSize (tx) {
286- s.logger .Warn ().Msg (" forced inclusion blob exceeds absolute maximum size - skipping" )
287- skippedTxs++
288- continue
289- }
290- ```
291-
292- ## Relationship with Executor
293-
294- ### DA Height Synchronization
295-
296- The executor maintains a separate ` DAHeight ` field in the blockchain state:
297-
298- ``` go
299- // In executor.go
300- newState.DAHeight = e.sequencer .GetDAHeight ()
301-
302- // State is saved after EVERY block
303- if err := batch.UpdateState (newState); err != nil {
304- return fmt.Errorf (" failed to update state: % w" , err)
305- }
306- ```
307-
308- ** Key Differences** :
309-
310- | Aspect | Based Sequencer Checkpoint | Executor State |
311- | --------| ---------------------------| ----------------|
312- | ** Frequency** | After every batch | After every block |
313- | ** Granularity** | DAHeight + TxIndex | DAHeight only |
314- | ** Purpose** | Track position in DA transaction stream | Track blockchain state |
315- | ** Storage** | Checkpoint datastore | State datastore |
316- | ** Scope** | Sequencer-specific | Chain-wide |
317-
318- ### Initialization Flow
319-
320- On startup:
321-
322- 1 . ** Executor** loads State from disk
323- 2 . ** Executor** calls ` sequencer.SetDAHeight(state.DAHeight) `
324- 3 . ** Based Sequencer** loads checkpoint from disk
325- 4 . If no checkpoint exists, initializes with current DA height
326- 5 . Both systems now synchronized
327-
328- ## Configuration
329-
330- ### Genesis Parameters
331-
332- ``` go
333- type Genesis struct {
334- // Starting DA height for the chain
335- DAStartHeight uint64
336-
337- // Number of DA blocks per epoch for forced inclusion
338- // Set to 0 to disable epochs (all blocks in epoch 1)
339- DAEpochForcedInclusion uint64
340- }
341- ```
342-
343- ### Example Configurations
344-
345- ** Frequent Fetching** (small epochs):
346- ``` go
347- DAStartHeight: 1000
348- DAEpochForcedInclusion: 1 // Fetch every DA block
349- ```
350-
351- ** Batched Fetching** (larger epochs):
352- ``` go
353- DAStartHeight: 1000
354- DAEpochForcedInclusion: 100 // Fetch every 100 DA blocks
355- ```
356-
357- ** Single Epoch** (no epoch boundaries):
358- ``` go
359- DAStartHeight: 1000
360- DAEpochForcedInclusion: 0 // All blocks in one epoch
361- ```
362-
363- ## Performance Considerations
364-
365- ### Memory Usage
366-
367- - ` currentBatchTxs ` holds all transactions from all DA heights in the current epoch
368- - With large epochs and many transactions, memory usage can be significant
369- - Example: Epoch size 100, 1000 txs/block, 1KB/tx = ~ 100MB
370-
371- ### DA Query Efficiency
372-
373- ** Pros** :
374- - Fewer DA queries (one per epoch instead of per block)
375- - Reduced DA layer costs
376-
377- ** Cons** :
378- - Longer wait times between transaction fetches
379- - Larger re-fetch overhead on crash recovery
380-
381- ### Crash Recovery Trade-offs
382-
383- ** Fine-grained checkpoints** (current approach):
384- - ✅ No transaction re-execution after crash
385- - ✅ Fast recovery within cached transactions
386- - ❌ Entire epoch re-fetched from DA
387- - ❌ All transactions re-validated
388-
389- ** Alternative** (epoch-level checkpoints):
390- - ✅ Simpler implementation
391- - ❌ All transactions in epoch re-executed after crash
392- - ❌ Longer recovery time
393-
394- The current design prioritizes ** no re-execution** over DA re-fetching, as execution is typically more expensive than fetching.
395-
396- ## Testing
397-
398- ### Unit Tests
399-
400- - ` checkpoint_test.go ` : Tests checkpoint persistence operations
401- - ` sequencer_test.go ` : Tests sequencer batch retrieval logic
402-
403- ### Integration Testing
404-
405- To test the based sequencer with a real DA layer:
406-
407- ``` bash
408- # Run with based sequencer configuration
409- make run-n NODES=1 SEQUENCER_TYPE=based
410-
411- # Simulate crash recovery
412- # 1. Stop node mid-epoch
413- # 2. Check checkpoint value
414- # 3. Restart node
415- # 4. Verify correct resumption
416- ```
417-
418- ## Debugging
419-
420- ### Log Messages
421-
422- ** Checkpoint Loading** :
423- ```
424- loaded based sequencer checkpoint from DB da_height=105 tx_index=3
425- ```
426-
427- ** DA Fetching** :
428- ```
429- fetching forced inclusion transactions from DA da_height=109
430- ```
431-
432- ** Not at Epoch End** :
433- ```
434- not at epoch end - returning empty transactions da_height=105 epoch_end=109
435- ```
436-
437- ** Transactions Retrieved** :
438- ```
439- fetched forced inclusion transactions from DA valid_tx_count=150 skipped_tx_count=2 da_height_start=100 da_height_end=109
440- ```
441-
442- ** Checkpoint Resumption** :
443- ```
444- resuming from checkpoint within DA block tx_index=3
445- ```
446-
447- ### Common Issues
448-
449- ** Problem** : No transactions being processed
450- - ** Check** : Are you at an epoch end? Transactions only arrive at epoch boundaries.
451- - ** Check** : Is forced inclusion configured? Look for ` ErrForceInclusionNotConfigured ` .
452-
453- ** Problem** : Transactions re-executed after restart
454- - ** Check** : Is checkpoint being persisted? Look for checkpoint save errors.
455- - ** Check** : Is checkpoint being loaded on restart?
456-
457- ** Problem** : Slow recovery after crash
458- - ** Cause** : Entire epoch is re-fetched from DA.
459- - ** Solution** : Reduce epoch size for faster recovery (at cost of more DA queries).
460-
461- ## Future Improvements
462-
463- ### Potential Optimizations
464-
465- 1 . ** Persistent Transaction Cache** : Store fetched transactions on disk to avoid re-fetching entire epoch after crash
466- 2 . ** Progressive Fetching** : Fetch DA blocks incrementally within an epoch instead of all at once
467- 3 . ** Compression** : Compress checkpoint data for faster I/O
468- 4 . ** Parallel Validation** : Validate transactions from multiple DA heights concurrently
469-
470- ### Design Alternatives
471-
472- 1 . ** Streaming Model** : Instead of epoch boundaries, stream transactions as DA blocks become available
473- 2 . ** Hybrid Checkpointing** : Save both fine-grained position and transaction cache
474- 3 . ** Two-Phase Commit** : Separate checkpoint updates from transaction processing for better crash consistency
475-
476- ## References
477-
478- - Core interfaces: ` core/sequencer.go `
479- - Forced inclusion: ` block/internal/da/forced_inclusion_retriever.go `
480- - Epoch calculations: ` types/epoch.go `
481- - Executor integration: ` block/internal/executing/executor.go `
204+ The checkpoint is serialized using Protocol Buffers (` pb.SequencerDACheckpoint ` ) for efficient storage and cross-version compatibility.
0 commit comments