Skip to content

Conversation

@joanestebanr
Copy link
Collaborator

πŸ”„ Changes Summary

  • [Brief description of what was added, fixed, or changed in this PR]

⚠️ Breaking Changes

  • πŸ› οΈ Config: [Optional: Changes to TOML config]
  • πŸ”Œ API/CLI: [Optional: Breaking interface changes]
  • πŸ—‘οΈ Deprecated Features: [Optional: Removed features]

πŸ“‹ Config Updates

  • 🧾 Diff/Config snippet: [Optional: Enumerate config changes/provide snippet of changes]

βœ… Testing

  • πŸ€– Automatic: [Optional: Enumerate E2E tests]
  • πŸ–±οΈ Manual: [Optional: Steps to verify]

🐞 Issues

  • Closes #[issue-number]

πŸ”— Related PRs

  • [Optional: Enumerate related pull requests]

πŸ“ Notes

  • [Optional: design decisions, tradeoffs, or TODOs]

@joanestebanr joanestebanr force-pushed the feature/multidownloader_reorg_unsafe_sync-v2 branch from 507d2fe to a4578b3 Compare January 19, 2026 14:59
@joanestebanr joanestebanr changed the title Feature/multidownloader reorg unsafe sync v2 feat: multidownloader reorg unsafe sync v2 Jan 19, 2026
@joanestebanr joanestebanr self-assigned this Jan 19, 2026
@joanestebanr joanestebanr requested a review from Copilot January 19, 2026 17:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements comprehensive reorganization handling and unsafe sync capabilities for the multidownloader component. The PR introduces new data structures and logic for detecting and processing blockchain reorganizations, separates synced data into finalized (safe) and non-finalized (unsafe) categories, and adds state management to track sync progress.

Changes:

  • Introduces reorg detection and processing infrastructure with new types (ReorgError, ReorgData, ReorgProcessor)
  • Implements unsafe sync mode that downloads non-finalized blocks with reorg checking
  • Adds comprehensive state management to separate synced and pending segments
  • Refactors storage layer with new tables for tracking reorged blocks and logs

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
types/map_block_header.go New type for mapping block numbers to headers
types/list_block_header.go New type for list of block headers with helper methods
types/block_header.go Added Empty() method to check if block header is empty
multidownloader/types/sync_segment.go Added Empty() and IsEmpty() methods for segment management
multidownloader/types/storager.go Extended interface with reorg and finalization support
multidownloader/types/reorg_*.go New types for reorg error handling and data structures
multidownloader/storage/storage_*.go Refactored storage layer with separate files for blocks, reorgs, and sync
multidownloader/storage/migrations/0002.sql New migration adding reorg tracking tables
multidownloader/state.go New state management for tracking synced vs pending segments
multidownloader/reorg_processor*.go New reorg detection and processing logic
multidownloader/evm_multidownloader.go Major refactoring with unsafe sync support and reorg handling
multidownloader/config.go Added PeriodToCheckReorgs configuration
Test files Updated and added tests for new functionality

Comment on lines +15 to 16
// If FromBlock is 0 means that is empty
BlockRange aggkitcommon.BlockRange
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at line 15 says "If FromBlock is 0 means that is empty" but the IsEmpty method checks if FromBlock > ToBlock to determine emptiness. These two approaches are inconsistent. The comment should be updated to match the actual implementation in IsEmpty, or the implementation should be changed to match the comment.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 41
CREATE TABLE logs_reorged (
chain_id BIGINT NOT NULL ,
block_number BIGINT NOT NULL ,
address TEXT NOT NULL, --
topics TEXT NOT NULL, -- list of hashes in JSON
data BLOB, --
tx_hash TEXT NOT NULL,
tx_index INTEGER NOT NULL,
log_index INTEGER NOT NULL, -- β€œindex” is a reserved keyword
PRIMARY KEY (address, chain_id,block_number, log_index),
FOREIGN KEY (chain_id, block_number) REFERENCES blocks_reorged(chain_id, block_number)
);

CREATE INDEX idx_logs_reorged_block_number ON logs_reorged(block_number);

CREATE TABLE blocks_reorged (
chain_id BIGINT NOT NULL REFERENCES reorgs(chain_id),
block_number BIGINT NOT NULL,
block_hash TEXT NOT NULL,
block_timestamp INTEGER NOT NULL,
block_parent_hash TEXT NOT NULL,
PRIMARY KEY (chain_id, block_number)
);

CREATE TABLE reorgs (
chain_id BIGINT PRIMARY KEY,
detected_at_block BIGINT NOT NULL,
reorged_from_block BIGINT NOT NULL,
reorged_to_block BIGINT NOT NULL,
detected_timestamp INTEGER NOT NULL,
network_latest_block INTEGER NOT NULL, -- which was the latest block in the detection moment
network_finalized_block INTEGER NOT NULL, -- which was the finalized block in the detection moment
network_finalized_block_name TEXT NOT NULL, -- name of the finalized block (e.g., "finalized", "safe", etc.)
description TEXT -- extran information, can be null
); No newline at end of file
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL migration has table creation order issues. The logs_reorged table references blocks_reorged via FOREIGN KEY at line 15, but blocks_reorged is defined later at line 20. Additionally, blocks_reorged references reorgs table at line 21, but reorgs is defined at line 29. In SQLite, tables must be created before they can be referenced in foreign key constraints. The order should be: reorgs, blocks_reorged, logs_reorged.

Copilot uses AI. Check for mistakes.
func (s *State) OnNewSyncedLogQuery(logQuery *mdrtypes.LogQuery) error {
err := s.Synced.AddLogQuery(logQuery)
if err != nil {
return fmt.Errorf("OnNewSyncedLogQuery: addding syned segment: %w", err)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at line 85 contains a typo: "addding syned segment" should be "adding synced segment".

Suggested change
return fmt.Errorf("OnNewSyncedLogQuery: addding syned segment: %w", err)
return fmt.Errorf("OnNewSyncedLogQuery: adding synced segment: %w", err)

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +166
// TODO: optimize this to don't check all blocks
// TODO: Find the first block to reorg
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment at line 165 mentions optimizing to not check all blocks and finding the first block to reorg. However, this is a critical performance concern for reorg detection. Consider creating a tracking issue for this optimization or removing the TODO if the current implementation is acceptable.

Suggested change
// TODO: optimize this to don't check all blocks
// TODO: Find the first block to reorg
// Compare all candidate blocks against RPC headers; return immediately on the first mismatch (reorg).
// Further performance optimizations (e.g., narrowing the checked range) can be evaluated separately if needed.

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 36
func (lbs ListBlockHeaders) BlockNumbers() []uint64 {
result := make([]uint64, 0, len(lbs))
for _, header := range lbs {
result = append(result, header.Number)
}
sort.Slice(result, func(i, j int) bool {
return result[i] < result[j]
})
return result
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function BlockNumbers creates a slice with initial capacity of len(lbs), then appends to it, and finally sorts it. However, the sorting is unnecessary if you're just collecting block numbers. Block numbers should already be in the order they appear in the list. If sorting is needed for a specific reason, add a comment explaining why. Additionally, consider if sorted block numbers are always required or if this could be an optional parameter.

Copilot uses AI. Check for mistakes.
  - Rename SyncerConfig.ContractsAddr to ContractAddresses for better naming consistency
    - Updated all references across multidownloader, l1infotreesync, and types packages
    - Updated all unit tests to use new field name

  Improvements:
  - Add auto-initialization in EVMMultidownloader.Start() when not already initialized
    - Adds safety check to call Initialize() if IsInitialized() returns false
    - Logs initialization attempt for better observability
  - Fix typo in log_query.go panic message: "unsuppoerted" β†’ "unsupported"
  - Add nil check for query.ToBlock in NewLogQueryFromEthereumFilter to prevent nil pointer panics
  - Fix typo in evm_multidownloader.go log message: "relauncing" β†’ "relaunching"

  Database:
  - Fix SQL migration 0002.sql:
    - Add missing DROP statements for blocks_reorged and reorgs tables in Down migration
    - Fix typo in comment: "extran" β†’ "extra"

  Types:
  - Update BlockHeader.Empty() to only check for nil (removed Number == 0 check)
  - Update NewListBlockHeadersEmpty to pre-allocate slice with nil elements
  - Add nil checks in ListBlockHeaders.ToMap() and BlockNumbers() methods
  - Add clarifying comment to ReorgError about OffendingBlockNumber behavior

  Testing:
  - Add unit test for Start() auto-initialization error handling
  - Re-enable TestEVMMultidownloader_Start with simplified test case
  - Update test contracts compilation scripts (logemitter)

  Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@joanestebanr joanestebanr force-pushed the feature/multidownloader_reorg_unsafe_sync-v2 branch from fd74186 to 4b119d1 Compare January 23, 2026 15:52
@joanestebanr
Copy link
Collaborator Author

@claude

@claude
Copy link

claude bot commented Jan 23, 2026

Claude finished @joanestebanr's task in 2m 10s β€”β€” View job


Code Review Complete βœ…

I've completed a thorough review of this PR implementing reorganization handling and unsafe sync capabilities for the multidownloader component. Here's my analysis:


πŸš€ What's New

  • Reorg Detection & Processing Infrastructure: Introduced comprehensive reorg handling with ReorgProcessor, ReorgError, and ReorgData types to detect and process blockchain reorganizations
  • Unsafe Sync Mode: Implemented unsafe sync that downloads non-finalized blocks with reorg checking, separating synced data into finalized (safe) and non-finalized (unsafe) categories
  • State Management: Added State struct to track synced vs pending segments with clear separation between safe and unsafe data
  • Storage Layer Enhancements: Refactored storage with new tables (reorgs, blocks_reorged, logs_reorged) and separated logic into storage_block.go, storage_reorg.go, storage_sync.go
  • Configuration: Added PeriodToCheckReorgs config option (default 5s) to control reorg check frequency

πŸ› Bug Fixes

None - this is a new feature implementation.

πŸ“‹ Config Updates

# New configuration field in multidownloader config:
PeriodToCheckReorgs = "5s"  # Default: 5 seconds
# Description: Duration to wait before checking for reorgs
# If set to 0, reorgs are checked only when a new block appears

⚠️ Breaking Changes

  • Config Field Rename: SyncerConfig.ContractsAddr β†’ ContractAddresses for better naming consistency (mentioned in PR description but not visible in this diff)
  • Storage Schema: New migration 0002.sql adds reorg tracking tables, which changes the database schema

πŸ”΄ Critical Issues (Security & Correctness)

1. SQL Migration Table Creation Order ⚠️

File: multidownloader/storage/migrations/0002.sql:7-29

The migration has foreign key constraint violations. Tables reference other tables before they're created:

  • Line 17: logs_reorged references blocks_reorged (FK defined before blocks_reorged exists)
  • Line 23: blocks_reorged references reorgs (FK defined before reorgs exists)

Tables must be created in this order: reorgs β†’ blocks_reorged β†’ logs_reorged

Fix table creation order β†’

2. Incomplete Migration Rollback

File: multidownloader/storage/migrations/0002.sql:1-4

The Down migration only drops logs_reorged but not blocks_reorged and reorgs. This leaves orphaned tables.

-- Current (incomplete):
DROP TABLE IF EXISTS logs_reorged;

-- Should be (complete rollback):
DROP TABLE IF EXISTS logs_reorged;
DROP TABLE IF EXISTS blocks_reorged;
DROP TABLE IF EXISTS reorgs;

Fix migration rollback β†’

3. Incorrect SQL Query for Lowest Block

File: multidownloader/storage/storage_block.go:108-111

The query for "lowest block" uses DESC (descending) order, which returns the highest block, not the lowest:

lowestBlock, err := a.getBlockHeadersNoMutex(tx, "SELECT * FROM blocks WHERE is_final=? "+
    "order by block_number DESC LIMIT 1", isFinal)  // ← Should be ASC

This will return incorrect results and break reorg detection logic.

Fix lowest block query β†’

4. Genesis Block Incorrectly Marked as Empty

File: types/block_header.go:37-39

func (gb *BlockHeader) Empty() bool {
    return gb == nil  // Only checks nil, not block 0
}

However, the comment at multidownloader/types/sync_segment.go:15 states: "If FromBlock is 0 means that is empty"

This creates semantic confusion. Block 0 (genesis) is a valid block. The Empty() check should be consistent:

  • Either document that number == 0 means empty (if intentional)
  • Or ensure Empty() only checks nil and update misleading comments

5. Error Handling with Panic in Production Code

File: multidownloader/evm_multidownloader.go:284-289

if reorgErr == nil {
    // TODO: Remove this panic and handle properly
    panic("Error running multidownloader: " + err.Error())
}

Using panic for error handling makes it impossible for callers to gracefully handle errors. This should return errors properly or at minimum document why panics are acceptable here.


🟑 Suggested Improvements (Code Quality & Maintainability)

6. Inconsistent Empty Check Logic

File: multidownloader/types/sync_segment.go:15,75-80

The comment says "If FromBlock is 0 means that is empty" but IsEmpty() checks FromBlock > ToBlock. The implementation and documentation disagree. Update the comment to match actual logic:

// IsEmpty returns true if the segment is empty (FromBlock > ToBlock)
func (s *SyncSegment) IsEmpty() bool {
    return s == nil || s.BlockRange.FromBlock > s.BlockRange.ToBlock
}

7. Typos in Comments and Error Messages

  • multidownloader/storage/migrations/0002.sql:40: "extran information" β†’ "extra information"
  • multidownloader/state.go:85: "addding syned segment" β†’ "adding synced segment"
  • multidownloader/evm_multidownloader.go:293: "relauncing" β†’ "relaunching"
  • multidownloader/storage/storage_reorg.go:40: "require a tx because it done multiples operations" β†’ "requires a tx because it performs multiple operations"
  • multidownloader/storage/storage_block.go:111: Error message says "GetRangeBlockHeader:highest:" but it's querying the lowest block

8. Commented-Out Code Should Be Removed

File: multidownloader/evm_multidownloader.go:493-519

20+ lines of commented code. Either:

  • Remove it entirely (use git history if needed later)
  • Add a comment explaining why it's preserved and when it might be used

9. Missing Documentation for nolint Directive

File: multidownloader/storage/storage_block.go:59

func (a *MultidownloaderStorage) saveAggkitBlock(tx dbtypes.Querier, //nolint:unparam

The //nolint:unparam directive lacks explanation. Document why tx parameter is always nil but must remain in the signature:

// tx is kept to allow transactional callers; currently always nil in practice
func (a *MultidownloaderStorage) saveAggkitBlock(tx dbtypes.Querier, //nolint:unparam

10. Unnecessary Sorting in BlockNumbers()

File: types/list_block_header.go:25-36

The BlockNumbers() function sorts the result, but there's no clear requirement for sorted output. If sorting is needed, document why. Otherwise, remove the sort for performance:

func (lbs ListBlockHeaders) BlockNumbers() []uint64 {
    result := make([]uint64, 0, len(lbs))
    for _, header := range lbs {
        if header != nil {
            result = append(result, header.Number)
        }
    }
    // Remove sort if not needed, or document why sorting is required
    sort.Slice(result, func(i, j int) bool {
        return result[i] < result[j]
    })
    return result
}

🟒 Minor Issues (Style & Conventions)

11. Skipped Test

File: multidownloader/evm_multidownloader_test.go:40

func TestEVMMultidownloader(t *testing.T) {
    t.Skip("code to test/debug not real unittest")

This test is always skipped. Either:

  • Remove the test if it's only for debugging
  • Add a clear comment explaining it's intentionally skipped for manual testing
  • Consider using build tags for manual tests

12. Test Name Typo

File: multidownloader/types/set_sync_segment_test.go:367

Test name has double space: "fulfill totally a segment" β†’ "fulfill totally a segment"

13. TODO Comments Need Action Items

Performance optimization TODO (evm_multidownloader.go:165-166):

// TODO: optimize this to don't check all blocks
// TODO: Find the first block to reorg

This is a critical performance concern for reorg detection. Either:

  • Create a tracking issue and reference it
  • Remove the TODO if current implementation is acceptable
  • Implement the optimization

βœ… Positive Observations

  1. Well-structured reorg handling: Clean separation of concerns with ReorgProcessor and dedicated port interface
  2. Comprehensive state management: The State struct with Synced and Pending segments provides clear data separation
  3. Good test coverage: Multiple test files for storage, state, and reorg logic
  4. Proper transaction handling: Consistent use of deferred rollbacks with commit flags
  5. Configuration flexibility: PeriodToCheckReorgs allows tuning reorg check frequency
  6. Following Go conventions: Proper error wrapping, use of fmt.Errorf with %w

πŸ“Š Summary

Category Count
πŸ”΄ Critical Issues 5
🟑 Suggested Improvements 6
🟒 Minor Issues 3
βœ… Positive Observations 6

Priority Actions:

  1. Fix SQL migration table order (blocks PR merge - will fail on SQLite)
  2. Fix lowest block query (causes incorrect behavior)
  3. Complete migration rollback (prevents clean downgrades)
  4. Document or fix genesis block handling (semantic clarity)
  5. Address panic in production code (production stability)

This is a solid implementation with good architecture. The critical issues are mostly fixable SQL problems that would prevent the migration from working correctly. Once addressed, this PR will provide robust reorg handling for the multidownloader.


| Branch: feature/multidownloader_reorg_unsafe_sync-v2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants