進階
Orphan Blocks
孤立區塊與過時區塊:比特幣網路中的區塊衝突處理
12 分鐘
概述
在分布式網路中,有時會同時產生多個有效區塊,導致臨時的鏈分叉。 這些未成為主鏈一部分的區塊被稱為「過時區塊」(stale blocks), 而其中的交易需要重新處理。理解這個機制對於交易安全性至關重要。
術語說明: 「Orphan block」現在更常指缺少父區塊的區塊。 被淘汰的區塊應稱為「stale block」。本文涵蓋兩種情況。
區塊類型
術語定義
區塊類型:
1. 有效區塊 (Valid Block)
- 滿足所有共識規則
- 在當前最長鏈上
2. 過時區塊 (Stale Block)
- 曾經有效,但不在最長鏈
- 被另一個區塊「取代」
- 也稱為「extinct block」
3. 孤立區塊 (Orphan Block) - 嚴格定義
- 收到了區塊,但缺少其父區塊
- 無法驗證和連接到鏈
- 暫時存放在 orphan pool
4. 叔塊 (Uncle Block) - 以太坊術語
- 比特幣沒有這個概念
- 以太坊獎勵這些區塊
視覺化:
Main Chain
┌─────────────────────────
┌────┐ │ ┌────┐ ┌────┐ ┌────┐
─────────│ B │───┼─│ C │───│ D │───│ E │
└────┘ │ └────┘ └────┘ └────┘
\ │
\ │
\ │
\│ ┌────┐
──│ C' │ ← Stale Block
└────┘ 產生原因
過時區塊產生原因:
1. 網路延遲
┌─────────────────────────────────────┐
│ 礦工 A (美國) 礦工 B (中國) │
│ │ │ │
│ 找到 C 找到 C' │
│ │ │ │
│ └───── 傳播 ────────┘ │
│ │
│ 部分網路收到 C,部分收到 C' │
└─────────────────────────────────────┘
2. 同時挖到
- 在同一秒內發現區塊
- 競爭傳播
3. 惡意行為
- 自私挖礦
- 雙花攻擊嘗試
網路統計:
- 約每 100-200 個區塊發生一次
- 取決於網路狀況和算力分布 衝突解決
最長鏈規則
比特幣選擇規則:
實際上是「最多工作量」規則:
- 累積難度最高的鏈獲勝
- 不是區塊數量最多的鏈
衝突解決過程:
時間 T1: 兩個區塊同時出現
┌────┐
│ C │
└────┘
┌────┐
│ C' │
└────┘
時間 T2: 節點各自選擇先收到的
Node 1: A → B → C
Node 2: A → B → C'
時間 T3: 下一個區塊決定勝負
如果在 C 之上找到 D:
A → B → C → D ← 勝出
A → B → C' ← 被放棄
所有節點重組到最長鏈 區塊重組 (Reorg)
重組過程:
起始狀態:
Node: A → B → C' (當前鏈尖)
收到: A → B → C → D
重組步驟:
1. 回滾 C'
- 從 UTXO 集合移除 C' 的效果
- 將 C' 的交易放回 mempool
2. 應用新鏈
- 連接 C
- 連接 D
- 更新 UTXO 集合
3. 處理交易
- C' 中的交易可能:
a) 也在 C 或 D 中 (已確認)
b) 不在新鏈中 (回到 mempool)
c) 與新鏈衝突 (被丟棄)
重組深度:
- 1 區塊重組: 常見
- 2+ 區塊重組: 少見
- 6+ 區塊重組: 極罕見 (可能是攻擊) Orphan Pool
孤立區塊處理
Orphan Pool (孤立區塊池):
場景:
收到區塊 D,但還沒有 C
?
┌────┐
│ D │ ← prevHash 指向未知區塊
└────┘
處理:
1. 無法驗證 (缺少父區塊)
2. 存入 orphan pool
3. 請求父區塊 (getdata)
4. 收到 C 後,驗證並連接 D
Bitcoin Core 限制:
- 最多 100 個孤立區塊
- 20 分鐘後過期
- 防止記憶體耗盡攻擊 孤立交易
Orphan Transactions (孤立交易):
定義:
- 引用了不存在的 UTXO
- 輸入的 prev_tx 未知
可能原因:
1. 父交易還沒收到
2. 父交易還沒確認
3. 父交易無效
處理:
1. 存入 orphan pool
2. 等待父交易
3. 父交易確認後重新處理
限制:
- 最多 100 個孤立交易
- 每個最大 100 KB
- 定期清理過期的
安全考量:
- 可被用於 DoS 攻擊
- 攻擊者發送大量無效孤立交易
- 因此有嚴格限制 TypeScript 實作
區塊鏈管理
interface BlockNode {
hash: string;
prevHash: string;
height: number;
totalWork: bigint;
header: BlockHeader;
}
class BlockIndex {
private blocks: Map<string, BlockNode> = new Map();
private orphans: Map<string, Block> = new Map();
private chainTip: BlockNode | null = null;
// 接收新區塊
async processBlock(block: Block): Promise<ProcessResult> {
const hash = computeBlockHash(block.header);
const prevHash = bytesToHex(block.header.prevHash);
// 檢查是否已存在
if (this.blocks.has(hash)) {
return { status: 'duplicate' };
}
// 檢查父區塊
const parent = this.blocks.get(prevHash);
if (!parent) {
// 存入 orphan pool
this.orphans.set(hash, block);
return { status: 'orphan', missingParent: prevHash };
}
// 驗證區塊
const validation = await this.validateBlock(block, parent);
if (!validation.valid) {
return { status: 'invalid', error: validation.error };
}
// 創建區塊節點
const node: BlockNode = {
hash,
prevHash,
height: parent.height + 1,
totalWork: parent.totalWork + calculateWork(block.header.bits),
header: block.header
};
this.blocks.set(hash, node);
// 檢查是否需要重組
if (node.totalWork > (this.chainTip?.totalWork ?? 0n)) {
await this.reorganize(node);
}
// 處理可能的孤立區塊
await this.processOrphans(hash);
return { status: 'accepted', height: node.height };
}
// 處理等待此區塊的孤立區塊
private async processOrphans(parentHash: string): Promise<void> {
const children: Block[] = [];
for (const [hash, block] of this.orphans) {
if (bytesToHex(block.header.prevHash) === parentHash) {
children.push(block);
this.orphans.delete(hash);
}
}
for (const child of children) {
await this.processBlock(child);
}
}
} 區塊重組
class BlockIndex {
// 區塊重組
private async reorganize(newTip: BlockNode): Promise<void> {
if (!this.chainTip) {
this.chainTip = newTip;
return;
}
// 找到共同祖先
const { ancestor, disconnectPath, connectPath } =
this.findForkPoint(this.chainTip, newTip);
console.log(`Reorganizing: disconnect ${disconnectPath.length}, ` +
`connect ${connectPath.length}`);
// 回滾舊鏈
const returnToMempool: Transaction[] = [];
for (const node of disconnectPath.reverse()) {
const block = await this.getBlockData(node.hash);
for (const tx of block.transactions.slice(1)) { // 跳過 coinbase
returnToMempool.push(tx);
}
await this.disconnectBlock(node);
}
// 連接新鏈
for (const node of connectPath) {
const block = await this.getBlockData(node.hash);
await this.connectBlock(node, block);
// 從 mempool 移除已確認的交易
for (const tx of block.transactions) {
this.mempool.remove(computeTxid(tx));
}
}
// 將有效交易放回 mempool
for (const tx of returnToMempool) {
try {
await this.mempool.add(tx);
} catch {
// 交易可能與新鏈衝突
}
}
this.chainTip = newTip;
}
// 找到分叉點
private findForkPoint(
tip1: BlockNode,
tip2: BlockNode
): {
ancestor: BlockNode;
disconnectPath: BlockNode[];
connectPath: BlockNode[];
} {
const disconnectPath: BlockNode[] = [];
const connectPath: BlockNode[] = [];
let node1 = tip1;
let node2 = tip2;
// 調整到相同高度
while (node1.height > node2.height) {
disconnectPath.push(node1);
node1 = this.blocks.get(node1.prevHash)!;
}
while (node2.height > node1.height) {
connectPath.push(node2);
node2 = this.blocks.get(node2.prevHash)!;
}
// 找到共同祖先
while (node1.hash !== node2.hash) {
disconnectPath.push(node1);
connectPath.push(node2);
node1 = this.blocks.get(node1.prevHash)!;
node2 = this.blocks.get(node2.prevHash)!;
}
return {
ancestor: node1,
disconnectPath,
connectPath: connectPath.reverse()
};
}
} 交易確認
確認數與安全性:
確認數 = 最長鏈高度 - 包含交易的區塊高度 + 1
0 確認:
- 交易在 mempool
- 可能被雙花
- 不要信任大額
1 確認:
- 包含在最新區塊
- 仍可能被重組
- 小額可接受
6 確認:
- 傳統「安全」標準
- 重組機率極低
- 大多數交易所要求
計算重組機率:
假設攻擊者有 q% 算力:
P(n 區塊重組) ≈ (q / (1-q))^n
範例 (攻擊者 10% 算力):
- 1 區塊: 11%
- 3 區塊: 0.13%
- 6 區塊: 0.00013%
大額交易建議等待更多確認 監控與偵測
interface ReorgEvent {
height: number;
depth: number;
oldTip: string;
newTip: string;
timestamp: number;
}
class ReorgMonitor {
private reorgHistory: ReorgEvent[] = [];
// 記錄重組事件
recordReorg(
height: number,
depth: number,
oldTip: string,
newTip: string
): void {
const event: ReorgEvent = {
height,
depth,
oldTip,
newTip,
timestamp: Date.now()
};
this.reorgHistory.push(event);
// 深度重組警告
if (depth >= 2) {
console.warn(`Deep reorg detected: ${depth} blocks at height ${height}`);
}
// 可能的攻擊警告
if (depth >= 6) {
console.error(`CRITICAL: Very deep reorg (${depth} blocks)!`);
this.notifyAdmin(event);
}
}
// 統計重組
getStats(): {
totalReorgs: number;
avgDepth: number;
maxDepth: number;
recentReorgs: ReorgEvent[];
} {
const recent = this.reorgHistory.filter(
e => e.timestamp > Date.now() - 24 * 60 * 60 * 1000
);
const depths = this.reorgHistory.map(e => e.depth);
return {
totalReorgs: this.reorgHistory.length,
avgDepth: depths.reduce((a, b) => a + b, 0) / depths.length,
maxDepth: Math.max(...depths),
recentReorgs: recent
};
}
private notifyAdmin(event: ReorgEvent): void {
// 發送警報
}
} 安全考量
相關攻擊:
1. 雙花攻擊
- 發送交易給商家
- 同時挖掘不包含該交易的區塊
- 如果攻擊者的鏈獲勝,交易被撤銷
2. 自私挖礦
- 挖到區塊不立即發布
- 等待適當時機釋放
- 浪費誠實礦工的工作
3. 51% 攻擊
- 控制多數算力
- 可以隨意重組
- 雙花任意交易
防禦措施:
1. 等待足夠確認
- 根據金額決定確認數
- 大額等待 6+ 確認
2. 監控算力分布
- 注意算力集中
- 警惕突然的算力變化
3. 使用檢查點
- 防止極深重組
- 保護歷史交易 相關資源
已複製連結