Skip to content

Commit 8eea476

Browse files
committed
Update BestBlock to store ANTI_REORG_DELAY * 2 recent block hashes
On restart, LDK expects the chain to be replayed starting from where it was when objects were last serialized. This is fine in the normal case, but if there was a reorg and the node which we were syncing from either resynced or was changed, the last block that we were synced as of might no longer be available. As a result, it becomes impossible to figure out where the fork point is, and thus to replay the chain. Luckily, changing the block source during a reorg isn't exactly common, but we shouldn't end up with a bricked node. To address this, `lightning-block-sync` allows the user to pass in `Cache` which can be used to cache recent blocks and thus allow for reorg handling in this case. However, serialization for, and a reasonable default implementation of a `Cache` was never built. Instead, here, we start taking a different approach. To avoid developers having to persist yet another object, we move `BestBlock` to storing some number of recent block hashes. This allows us to find the fork point with just the serialized state. In conjunction with 403dc1a (which allows us to disconnect blocks without having the stored header), this should allow us to replay chain state after a reorg even if we no longer have access to the top few blocks of the old chain tip. While we only really need to store `ANTI_REORG_DELAY` blocks (as we generally assume that any deeper reorg won't happen and thus we don't guarantee we handle it correctly), its nice to store a few more to be able to handle more than a six block reorg. While other parts of the codebase may not be entirely robust against such a reorg if the transactions confirmed change out from under us, its entirely possible (and, indeed, common) for reorgs to contain nearly identical transactions.
1 parent 927edf1 commit 8eea476

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

lightning/src/chain/mod.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use bitcoin::network::Network;
1818
use bitcoin::script::{Script, ScriptBuf};
1919
use bitcoin::secp256k1::PublicKey;
2020

21-
use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, MonitorEvent};
21+
use crate::chain::channelmonitor::{
22+
ChannelMonitor, ChannelMonitorUpdate, MonitorEvent, ANTI_REORG_DELAY,
23+
};
2224
use crate::chain::transaction::{OutPoint, TransactionData};
2325
use crate::ln::types::ChannelId;
2426
use crate::sign::ecdsa::EcdsaChannelSigner;
@@ -43,27 +45,96 @@ pub struct BestBlock {
4345
pub block_hash: BlockHash,
4446
/// The height at which the block was confirmed.
4547
pub height: u32,
48+
/// Previous blocks immediately before [`Self::block_hash`], in reverse chronological order.
49+
///
50+
/// These ensure we can find the fork point of a reorg if our block source no longer has the
51+
/// previous best tip after a restart.
52+
pub previous_blocks: [Option<BlockHash>; ANTI_REORG_DELAY as usize * 2],
4653
}
4754

4855
impl BestBlock {
4956
/// Constructs a `BestBlock` that represents the genesis block at height 0 of the given
5057
/// network.
5158
pub fn from_network(network: Network) -> Self {
52-
BestBlock { block_hash: genesis_block(network).header.block_hash(), height: 0 }
59+
let block_hash = genesis_block(network).header.block_hash();
60+
let previous_blocks = [None; ANTI_REORG_DELAY as usize * 2];
61+
BestBlock { block_hash, height: 0, previous_blocks }
5362
}
5463

5564
/// Returns a `BestBlock` as identified by the given block hash and height.
5665
///
5766
/// This is not exported to bindings users directly as the bindings auto-generate an
5867
/// equivalent `new`.
5968
pub fn new(block_hash: BlockHash, height: u32) -> Self {
60-
BestBlock { block_hash, height }
69+
let previous_blocks = [None; ANTI_REORG_DELAY as usize * 2];
70+
BestBlock { block_hash, height, previous_blocks }
71+
}
72+
73+
/// Advances to a new block at height [`Self::height`] + 1.
74+
pub fn advance(&mut self, new_hash: BlockHash) {
75+
// Shift all block hashes to the right (making room for the old tip at index 0)
76+
for i in (1..self.previous_blocks.len()).rev() {
77+
self.previous_blocks[i] = self.previous_blocks[i - 1];
78+
}
79+
80+
// The old tip becomes the new index 0 (tip-1)
81+
self.previous_blocks[0] = Some(self.block_hash);
82+
83+
// Update to the new tip
84+
self.block_hash = new_hash;
85+
self.height += 1;
86+
}
87+
88+
/// Returns the block hash at the given height, if available in our history.
89+
pub fn get_hash_at_height(&self, height: u32) -> Option<BlockHash> {
90+
if height > self.height {
91+
return None;
92+
}
93+
if height == self.height {
94+
return Some(self.block_hash);
95+
}
96+
97+
// offset = 1 means we want tip-1, which is block_hashes[0]
98+
// offset = 2 means we want tip-2, which is block_hashes[1], etc.
99+
let offset = self.height.saturating_sub(height) as usize;
100+
if offset >= 1 && offset <= self.previous_blocks.len() {
101+
self.previous_blocks[offset - 1]
102+
} else {
103+
None
104+
}
105+
}
106+
107+
/// Find the most recent common ancestor between two BestBlocks by searching their block hash
108+
/// histories.
109+
///
110+
/// Returns the common block hash and height, or None if no common block is found in the
111+
/// available histories.
112+
pub fn find_common_ancestor(&self, other: &BestBlock) -> Option<(BlockHash, u32)> {
113+
// First check if either tip matches
114+
if self.block_hash == other.block_hash && self.height == other.height {
115+
return Some((self.block_hash, self.height));
116+
}
117+
118+
// Check all heights covered by self's history
119+
let min_height = self.height.saturating_sub(self.previous_blocks.len() as u32);
120+
for check_height in (min_height..=self.height).rev() {
121+
if let Some(self_hash) = self.get_hash_at_height(check_height) {
122+
if let Some(other_hash) = other.get_hash_at_height(check_height) {
123+
if self_hash == other_hash {
124+
return Some((self_hash, check_height));
125+
}
126+
}
127+
}
128+
}
129+
None
61130
}
62131
}
63132

64133
impl_writeable_tlv_based!(BestBlock, {
65134
(0, block_hash, required),
135+
(1, previous_blocks_read, (legacy, [Option<BlockHash>; ANTI_REORG_DELAY as usize * 2], |us: &BestBlock| Some(us.previous_blocks))),
66136
(2, height, required),
137+
(unused, previous_blocks, (static_value, previous_blocks_read.unwrap_or([None; ANTI_REORG_DELAY as usize * 2]))),
67138
});
68139

69140
/// The `Listen` trait is used to notify when blocks have been connected or disconnected from the

lightning/src/util/ser.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,33 @@ impl Readable for BlockHash {
14481448
}
14491449
}
14501450

1451+
impl Writeable for [Option<BlockHash>; 12] {
1452+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
1453+
for hash_opt in self {
1454+
match hash_opt {
1455+
Some(hash) => hash.write(w)?,
1456+
None => ([0u8; 32]).write(w)?,
1457+
}
1458+
}
1459+
Ok(())
1460+
}
1461+
}
1462+
1463+
impl Readable for [Option<BlockHash>; 12] {
1464+
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
1465+
use bitcoin::hashes::Hash;
1466+
1467+
let mut res = [None; 12];
1468+
for hash_opt in res.iter_mut() {
1469+
let buf: [u8; 32] = Readable::read(r)?;
1470+
if buf != [0; 32] {
1471+
*hash_opt = Some(BlockHash::from_slice(&buf[..]).unwrap());
1472+
}
1473+
}
1474+
Ok(res)
1475+
}
1476+
}
1477+
14511478
impl Writeable for ChainHash {
14521479
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
14531480
w.write_all(self.as_bytes())

0 commit comments

Comments
 (0)