opnet
Version:
The perfect library for building Bitcoin-based applications.
530 lines (414 loc) • 14 kB
Markdown
# Reorg Detection
This guide covers detecting and handling blockchain reorganizations on OPNet.
## Overview
A blockchain reorganization (reorg) occurs when a longer chain replaces the current chain, invalidating previously confirmed blocks. Detecting and handling reorgs is essential for reliable applications.
```mermaid
flowchart TD
subgraph "Original Chain"
A[Block 100] --> B[Block 101]
B --> C[Block 102]
end
subgraph "After Reorg"
A --> D[Block 101']
D --> E[Block 102']
E --> F[Block 103']
end
C -.->|Orphaned| X[Discarded]
```
---
## Checking for Reorgs
### Basic Reorg Query
```typescript
import { JSONRpcProvider } from 'opnet';
import { networks } from '@btc-vision/bitcoin';
const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });
// Check for reorgs in a block range
const reorgs = await provider.getReorg(100n, 200n);
if (reorgs.length > 0) {
console.log('Reorgs detected:', reorgs.length);
for (const reorg of reorgs) {
console.log(' From block:', reorg.fromBlock);
console.log(' To block:', reorg.toBlock);
console.log(' Timestamp:', reorg.timestamp);
}
} else {
console.log('No reorgs in range');
}
```
### Method Signature
```typescript
async getReorg(
fromBlock?: BigNumberish, // Start of range
toBlock?: BigNumberish // End of range
): Promise<ReorgInformation[]>
```
## ReorgInformation Structure
```typescript
interface ReorgInformation {
fromBlock: string | bigint; // Start block of reorg range
toBlock: string | bigint; // End block of reorg range
readonly timestamp: number; // When reorg was detected
}
```
## Reorg Detection Strategies
### Monitor for Recent Reorgs
```typescript
async function checkRecentReorgs(
provider: JSONRpcProvider,
blockCount: number = 10
): Promise<ReorgInformation[]> {
const currentBlock = await provider.getBlockNumber();
const fromBlock = currentBlock - BigInt(blockCount);
return provider.getReorg(fromBlock, currentBlock);
}
// Usage
const recentReorgs = await checkRecentReorgs(provider, 100);
if (recentReorgs.length > 0) {
console.log('Recent reorgs detected!');
}
```
### Continuous Reorg Monitoring
```typescript
async function monitorReorgs(
provider: JSONRpcProvider,
callback: (reorg: ReorgInformation) => void,
intervalMs: number = 30000
): Promise<() => void> {
let lastCheckedBlock = await provider.getBlockNumber();
const intervalId = setInterval(async () => {
try {
const currentBlock = await provider.getBlockNumber();
if (currentBlock > lastCheckedBlock) {
const reorgs = await provider.getReorg(
lastCheckedBlock,
currentBlock
);
for (const reorg of reorgs) {
callback(reorg);
}
lastCheckedBlock = currentBlock;
}
} catch (error) {
console.error('Error checking reorgs:', error);
}
}, intervalMs);
return () => clearInterval(intervalId);
}
// Usage
const stopMonitoring = await monitorReorgs(provider, (reorg) => {
console.log(`REORG: Blocks ${reorg.fromBlock} to ${reorg.toBlock} affected!`);
console.log(` Timestamp: ${reorg.timestamp}`);
});
// Later: stop monitoring
// stopMonitoring();
```
## Handling Reorgs
### Verify Transaction After Reorg
```typescript
async function isTransactionStillValid(
provider: JSONRpcProvider,
txHash: string,
expectedBlockNumber: bigint
): Promise<boolean> {
try {
// Check for reorgs affecting this block
const reorgs = await provider.getReorg(
expectedBlockNumber,
expectedBlockNumber
);
if (reorgs.length > 0) {
// Block was reorganized, check if TX still exists
try {
const tx = await provider.getTransaction(txHash);
return tx !== null;
} catch {
return false;
}
}
return true;
} catch {
return false;
}
}
// Usage
const txValid = await isTransactionStillValid(
provider,
'0x123...txhash...',
123456n
);
if (!txValid) {
console.log('Transaction may have been reverted by reorg!');
}
```
### Reorg-Safe Confirmation
```typescript
interface ConfirmationStatus {
confirmed: boolean;
confirmations: number;
reorgRisk: boolean;
message: string;
}
async function getConfirmationStatus(
provider: JSONRpcProvider,
txBlockNumber: bigint,
requiredConfirmations: number = 6
): Promise<ConfirmationStatus> {
const currentBlock = await provider.getBlockNumber();
const confirmations = Number(currentBlock - txBlockNumber);
// Check for any reorgs in the confirmation range
const reorgs = await provider.getReorg(txBlockNumber, currentBlock);
const reorgRisk = reorgs.length > 0;
const confirmed = confirmations >= requiredConfirmations && !reorgRisk;
let message: string;
if (reorgRisk) {
message = `Reorg detected affecting blocks ${reorgs.map(r => `${r.fromBlock}-${r.toBlock}`).join(', ')}`;
} else if (confirmed) {
message = `Confirmed with ${confirmations} confirmations`;
} else {
message = `Waiting for confirmations (${confirmations}/${requiredConfirmations})`;
}
return {
confirmed,
confirmations,
reorgRisk,
message,
};
}
// Usage
const status = await getConfirmationStatus(provider, 123456n, 6);
console.log('Status:', status.message);
```
## Block Hash Tracking
### Track Known Block Hashes
```typescript
class BlockHashTracker {
private knownHashes: Map<bigint, string> = new Map();
async updateFromProvider(
provider: JSONRpcProvider,
startBlock: bigint,
endBlock: bigint
): Promise<void> {
for (let i = startBlock; i <= endBlock; i++) {
const block = await provider.getBlock(i);
this.knownHashes.set(BigInt(block.height), block.hash);
}
}
async detectReorgs(
provider: JSONRpcProvider
): Promise<Array<{ blockNumber: bigint; expected: string; actual: string }>> {
const reorgs: Array<{ blockNumber: bigint; expected: string; actual: string }> = [];
for (const [blockNumber, expectedHash] of this.knownHashes) {
try {
const block = await provider.getBlock(blockNumber);
if (block.hash !== expectedHash) {
reorgs.push({
blockNumber,
expected: expectedHash,
actual: block.hash,
});
// Update to new hash
this.knownHashes.set(blockNumber, block.hash);
}
} catch {
// Block might not exist anymore
reorgs.push({
blockNumber,
expected: expectedHash,
actual: 'MISSING',
});
}
}
return reorgs;
}
getHash(blockNumber: bigint): string | undefined {
return this.knownHashes.get(blockNumber);
}
clear(): void {
this.knownHashes.clear();
}
}
// Usage
const tracker = new BlockHashTracker();
// Track blocks 100-110
await tracker.updateFromProvider(provider, 100n, 110n);
// Later, check for reorgs
const detected = await tracker.detectReorgs(provider);
if (detected.length > 0) {
console.log('Detected reorgs:', detected);
}
```
## Reorg Recovery
### Handle Application State After Reorg
```typescript
interface TransactionRecord {
txHash: string;
blockNumber: bigint;
blockHash: string;
status: 'pending' | 'confirmed' | 'failed' | 'reorged';
}
class ReorgAwareTransactionTracker {
private transactions: Map<string, TransactionRecord> = new Map();
addTransaction(tx: TransactionRecord): void {
this.transactions.set(tx.txHash, tx);
}
async checkAllTransactions(
provider: JSONRpcProvider
): Promise<TransactionRecord[]> {
const reorgedTxs: TransactionRecord[] = [];
for (const [txHash, record] of this.transactions) {
if (record.status === 'confirmed') {
const reorgs = await provider.getReorg(
record.blockNumber,
record.blockNumber
);
if (reorgs.length > 0) {
// Check if transaction is still valid
try {
const tx = await provider.getTransaction(txHash);
if (!tx) {
record.status = 'reorged';
reorgedTxs.push(record);
}
} catch {
record.status = 'reorged';
reorgedTxs.push(record);
}
}
}
}
return reorgedTxs;
}
getReorgedTransactions(): TransactionRecord[] {
return Array.from(this.transactions.values())
.filter(tx => tx.status === 'reorged');
}
}
// Usage
const txTracker = new ReorgAwareTransactionTracker();
// Add confirmed transaction
txTracker.addTransaction({
txHash: '0x123...',
blockNumber: 123456n,
blockHash: '0xabc...',
status: 'confirmed',
});
// Check for reorgs affecting our transactions
const reorgedTxs = await txTracker.checkAllTransactions(provider);
if (reorgedTxs.length > 0) {
console.log('Transactions need to be resubmitted:', reorgedTxs);
}
```
## Complete Reorg Service
```typescript
class ReorgService {
private provider: JSONRpcProvider;
private callbacks: ((reorg: ReorgInformation) => void)[] = [];
private monitoringInterval: ReturnType<typeof setInterval> | null = null;
private lastCheckedBlock: bigint = 0n;
constructor(provider: JSONRpcProvider) {
this.provider = provider;
}
async checkRange(
fromBlock: bigint,
toBlock: bigint
): Promise<ReorgInformation[]> {
return this.provider.getReorg(fromBlock, toBlock);
}
async checkRecent(blockCount: number = 10): Promise<ReorgInformation[]> {
const current = await this.provider.getBlockNumber();
const from = current - BigInt(blockCount);
return this.checkRange(from, current);
}
async wasBlockReorged(blockNumber: bigint): Promise<boolean> {
const reorgs = await this.checkRange(blockNumber, blockNumber);
return reorgs.length > 0;
}
onReorg(callback: (reorg: ReorgInformation) => void): () => void {
this.callbacks.push(callback);
return () => {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
};
}
startMonitoring(intervalMs: number = 30000): void {
if (this.monitoringInterval) return;
this.provider.getBlockNumber().then((block) => {
this.lastCheckedBlock = block;
});
this.monitoringInterval = setInterval(async () => {
try {
const current = await this.provider.getBlockNumber();
if (current > this.lastCheckedBlock) {
const reorgs = await this.checkRange(
this.lastCheckedBlock,
current
);
for (const reorg of reorgs) {
for (const callback of this.callbacks) {
callback(reorg);
}
}
this.lastCheckedBlock = current;
}
} catch (error) {
console.error('Reorg monitoring error:', error);
}
}, intervalMs);
}
stopMonitoring(): void {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
}
}
}
// Usage
const reorgService = new ReorgService(provider);
// Register callback
const unsubscribe = reorgService.onReorg((reorg) => {
console.log(`REORG ALERT: Blocks ${reorg.fromBlock} to ${reorg.toBlock}`);
// Handle reorg: invalidate caches, check transactions, etc.
});
// Start monitoring
reorgService.startMonitoring(10000);
// Check specific block
const wasReorged = await reorgService.wasBlockReorged(123456n);
console.log('Block 123456 reorged:', wasReorged);
// Check recent blocks
const recent = await reorgService.checkRecent(50);
console.log('Recent reorgs:', recent.length);
// Cleanup
// reorgService.stopMonitoring();
// unsubscribe();
```
## Best Practices
1. **Wait for Confirmations**: More confirmations = lower reorg risk
2. **Monitor Critical Blocks**: Track blocks containing important transactions
3. **Handle Gracefully**: Don't crash on reorg, recover state
4. **Cache Invalidation**: Clear caches on reorg detection
5. **User Notification**: Inform users if their transactions are affected
## Confirmation Recommendations
| Risk Level | Confirmations | Use Case |
|------------|---------------|----------|
| Low | 1-2 | Read-only queries |
| Medium | 3-6 | Small value transactions |
| High | 6-12 | Large value transactions |
| Critical | 12+ | Exchange deposits |
## Next Steps
- [Block Operations](./block-operations.md) - Fetching blocks
- [Block Witnesses](./block-witnesses.md) - Witness validation
- [Transaction Receipts](../transactions/transaction-receipts.md) - Receipt handling
[← Previous: Block Witnesses](./block-witnesses.md) | [Next: Epoch Overview →](../epochs/overview.md)