Skip to content

Commit 6890d32

Browse files
committed
update docs
1 parent 87f9da5 commit 6890d32

1 file changed

Lines changed: 14 additions & 291 deletions

File tree

sequencers/based/README.md

Lines changed: 14 additions & 291 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The Based Sequencer implements the `Sequencer` interface from `core/sequencer.go
2929
Transactions 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
7778
type 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
```
9193
Checkpoint: (DAHeight: 100, TxIndex: 0)
9294
- Ready to fetch epoch starting at DA height 100
9395
```
9496

9597
#### 2. Fetching Transactions
98+
9699
When `GetNextBatch()` is called and we're at an epoch end:
100+
97101
```
98102
Request: GetNextBatch(maxBytes: 1MB)
99103
Action: Fetch all transactions from epoch (DA heights 100-109)
100104
Result: currentBatchTxs = [tx1, tx2, tx3, ..., txN] (from entire epoch)
101105
```
102106

103107
#### 3. Processing Transactions
108+
104109
Transactions are processed incrementally, respecting `maxBytes`:
110+
105111
```
106112
Batch 1: [tx1, tx2] (fits in maxBytes)
107113
Checkpoint: (DAHeight: 100, TxIndex: 2)
@@ -122,15 +128,15 @@ Checkpoint: (DAHeight: 101, TxIndex: 0)
122128
```go
123129
if 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
191201
err := 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

Comments
 (0)