Skip to content

EVM-20: ReorgHandler domain service #160

@Puneethkumarck

Description

@Puneethkumarck

Context

Ethereum and EVM-compatible chains experience chain reorganizations (reorgs) where a competing fork replaces the canonical chain. When a reorg is detected, the indexer must roll back all data above the fork point: blocks, transactions, failed transactions, large transfers, and token transfers. The `ReorgHandler` orchestrates this by draining in-flight writes and delegating the atomic cascade delete to a `ReorgCascadeDeleter` port.

Specification

File

`prism/src/main/java/com/stablebridge/prism/domain/service/ReorgHandler.java`

Constructor dependencies

  • `EvmBlockRepository` — for block lookup during reorg detection
  • `EvmTransactionBatchService` — to drain (flush) in-flight writes before deleting
  • `ReorgCascadeDeleter` — port interface for atomic cascade delete across all EVM tables

ReorgCascadeDeleter port interface

```java
/**

  • Port interface for atomic cascade deletion during chain reorganization.
  • Infrastructure implementation (EVM-29) wraps this in a single DB transaction.
    */
    public interface ReorgCascadeDeleter {
    void deleteAboveBlock(long blockNumber);
    }
    ```

This port lives in `domain/port/`. The infrastructure implementation (`ReorgCascadeDeleter` from EVM-29) opens a single DB connection, runs BEGIN, deletes from 5 tables in order (token_transfers, large_transfers, failed_transactions, transactions, blocks), and COMMITs. On error it rolls back.

Methods

handleReorg
```java
/**

  • Handle a chain reorganization by deleting all data above the fork block.

  • @param forkBlock the block number where the fork occurred (exclusive —

  •              data AT this block is kept, data ABOVE is deleted)
    
  • @return the reorg depth (number of blocks rolled back)
    */
    public int handleReorg(long forkBlock) {
    // 1. Drain the batch queue to prevent in-flight writes to reorged blocks
    batchService.flush();

    // 2. Delegate atomic cascade delete to ReorgCascadeDeleter port
    // (infrastructure implementation wraps in single DB transaction)
    cascadeDeleter.deleteAboveBlock(forkBlock);

    // 3. Return the depth (caller calculates from current head vs fork block)
    return 0; // placeholder — caller sets correct depth
    }
    ```

Note: The domain service does NOT call individual repository `deleteAboveBlock` methods directly. Instead, it delegates to the `ReorgCascadeDeleter` port interface, whose infrastructure implementation (EVM-29) handles the atomic multi-table delete in a single DB transaction.

detectReorg
```java
/**

  • Detect a potential reorganization by comparing the incoming block's parent hash
  • against the expected parent hash from the tracker.
  • @param blockNumber the incoming block number
  • @param parentHash the incoming block's parent hash
  • @param tracker the local parent hash tracker (maps blockNumber -> blockHash)
  • @return the fork block number if a reorg is detected, empty otherwise
    */
    public Optional detectReorg(long blockNumber, String parentHash,
    ParentHashTracker tracker) {
    var expectedHash = tracker.getHash(blockNumber - 1);
    if (expectedHash.isEmpty()) {
    return Optional.empty(); // no history — cannot detect reorg
    }
    if (expectedHash.get().equals(parentHash)) {
    return Optional.empty(); // parent hash matches — no reorg
    }
    // Parent hash mismatch — walk back to find fork point
    return findForkPoint(blockNumber, tracker);
    }

private Optional findForkPoint(long fromBlock, ParentHashTracker tracker) {
// Walk backwards through tracker history to find where chains diverge
// Returns the last block number where hashes still match
// Limited by maxReorgDepth from ChainConfig
// ...
}
```

ParentHashTracker

A simple interface that the reorg handler uses to look up previously seen block hashes:

```java
public interface ParentHashTracker {
Optional getHash(long blockNumber);
void recordHash(long blockNumber, String blockHash);
}
```

This can be an in-memory ring buffer (infrastructure implementation, EVM-35) or backed by the `EvmBlockRepository`.

Design decisions

  • The handler does NOT call individual repository `deleteAboveBlock` methods — it delegates to `ReorgCascadeDeleter` for atomicity
  • `ReorgCascadeDeleter` is a domain port interface; the infrastructure implementation (EVM-29) wraps the cascade in a single DB transaction
  • `handleReorg` first drains the batch queue via `EvmTransactionBatchService.flush()` to prevent in-flight writes from re-inserting orphaned data
  • `detectReorg` uses a `ParentHashTracker` abstraction to decouple from specific storage
  • Fork point detection walks backwards from the current block, limited by `maxReorgDepth`
  • `@Singleton`, `@RequiredArgsConstructor`, `@Slf4j`
  • Account table is NOT rolled back — cumulative counts remain (acceptable approximation for v1, per functional spec section 4)

Test class

`prism/src/test/java/com/stablebridge/prism/domain/service/ReorgHandlerTest.java`

Test cases:

  • `handleReorg` calls `batchService.flush()` before cascade delete
  • `handleReorg` calls `cascadeDeleter.deleteAboveBlock()` with the correct block number
  • `handleReorg` does NOT call individual repository `deleteAboveBlock` methods
  • `detectReorg` returns empty when parent hash matches (no reorg)
  • `detectReorg` returns empty when tracker has no history for the block
  • `detectReorg` returns fork block when parent hash mismatches
  • Fork point detection walks back correctly through tracker history

Acceptance Criteria

  • `ReorgHandler` class exists with constructor dependencies: `EvmBlockRepository`, `EvmTransactionBatchService`, `ReorgCascadeDeleter`
  • `ReorgCascadeDeleter` port interface defined in `domain/port/`
  • `handleReorg(long forkBlock)` drains batch queue first, then delegates to `ReorgCascadeDeleter`
  • Does NOT call individual repository `deleteAboveBlock` methods
  • `detectReorg` correctly identifies reorgs via parent hash mismatch
  • `ParentHashTracker` interface is defined for hash lookup abstraction
  • Fork point detection walks backwards through block history
  • Zero framework imports in domain layer (except Avaje/Lombok annotations)
  • All test cases pass
  • `./gradlew build` passes

Dependencies

References

Metadata

Metadata

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions