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
Dependencies
References
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
ReorgCascadeDeleter port interface
```java
/**
*/
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 —
@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
/**
*/
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
Test class
`prism/src/test/java/com/stablebridge/prism/domain/service/ReorgHandlerTest.java`
Test cases:
Acceptance Criteria
Dependencies
References