Skip to content

Commit 72cf6f0

Browse files
shesekphilippem
authored andcommitted
Fix handling of reorgs that shorten the chain
Prior to this fix, `Indexer::update()` would panic on the `assert_eq!(tip, *headers.tip())` assertion when handling reorgs that shorten the existing chain without adding any blocks to replace them. This should not normally happen, but might due to manual `invalidateblock`. For example, this will reproduce the panic: `bitcoin-cli invalidateblock $(bitcoin-cli getbestblockhash)`
1 parent d0f71c5 commit 72cf6f0

3 files changed

Lines changed: 56 additions & 48 deletions

File tree

src/new_index/schema.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ impl Indexer {
284284
tip: &BlockHash,
285285
) -> Result<(Vec<HeaderEntry>, Option<usize>)> {
286286
let indexed_headers = self.store.indexed_headers.read().unwrap();
287-
let raw_new_headers = daemon.get_new_headers(&indexed_headers, &tip)?;
288-
let (new_headers, reorged_since) = indexed_headers.preprocess(raw_new_headers);
287+
let raw_new_headers = daemon.get_new_headers(&indexed_headers, tip)?;
288+
let (new_headers, reorged_since) = indexed_headers.preprocess(raw_new_headers, tip);
289289

290290
if let Some(tip) = new_headers.last() {
291291
info!("{:?} ({} left to index)", tip, new_headers.len());

src/util/block.rs

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use crate::chain::{BlockHash, BlockHeader};
22
use crate::errors::*;
33
use crate::new_index::BlockEntry;
44

5+
use itertools::Itertools;
56
use std::collections::HashMap;
67
use std::fmt;
7-
use std::iter::FromIterator;
88
use std::slice;
99
use time::format_description::well_known::Rfc3339;
1010
use time::OffsetDateTime as DateTime;
@@ -128,7 +128,7 @@ impl HeaderList {
128128
);
129129

130130
let mut headers = HeaderList::empty();
131-
headers.append(headers.preprocess(headers_chain).0);
131+
headers.append(headers.preprocess(headers_chain, &tip_hash).0);
132132
headers
133133
}
134134

@@ -138,43 +138,50 @@ impl HeaderList {
138138
/// Actually applying the headers requires to first pop() the reorged blocks (if any),
139139
/// then append() the new ones.
140140
#[trace]
141-
pub fn preprocess(&self, new_headers: Vec<BlockHeader>) -> (Vec<HeaderEntry>, Option<usize>) {
141+
pub fn preprocess(
142+
&self,
143+
new_headers: Vec<BlockHeader>,
144+
new_tip: &BlockHash,
145+
) -> (Vec<HeaderEntry>, Option<usize>) {
142146
// header[i] -> header[i-1] (i.e. header.last() is the tip)
143-
struct HashedHeader {
144-
blockhash: BlockHash,
145-
header: BlockHeader,
146-
}
147-
let hashed_headers =
148-
Vec::<HashedHeader>::from_iter(new_headers.into_iter().map(|header| HashedHeader {
149-
blockhash: header.block_hash(),
150-
header,
151-
}));
152-
for i in 1..hashed_headers.len() {
153-
assert_eq!(
154-
hashed_headers[i].header.prev_blockhash,
155-
hashed_headers[i - 1].blockhash
156-
);
157-
}
158-
let prev_blockhash = match hashed_headers.first() {
159-
Some(h) => h.header.prev_blockhash,
160-
None => return (vec![], None), // hashed_headers is empty
161-
};
162-
let new_height: usize = if prev_blockhash == *DEFAULT_BLOCKHASH {
163-
0
147+
let (new_height, header_entries) = if !new_headers.is_empty() {
148+
let hashed_headers = new_headers
149+
.into_iter()
150+
.map(|h| (h.block_hash(), h))
151+
.collect::<Vec<_>>();
152+
for ((curr_blockhash, _), (_, next_header)) in hashed_headers.iter().tuple_windows() {
153+
assert_eq!(*curr_blockhash, next_header.prev_blockhash);
154+
}
155+
assert_eq!(hashed_headers.last().unwrap().0, *new_tip);
156+
157+
let prev_blockhash = &hashed_headers.first().unwrap().1.prev_blockhash;
158+
let new_height = if *prev_blockhash == *DEFAULT_BLOCKHASH {
159+
0
160+
} else {
161+
self.header_by_blockhash(prev_blockhash)
162+
.expect("headers do not connect")
163+
.height()
164+
+ 1
165+
};
166+
let header_entries = (new_height..)
167+
.zip(hashed_headers)
168+
.map(|(height, (hash, header))| HeaderEntry {
169+
height,
170+
hash,
171+
header,
172+
})
173+
.collect();
174+
(new_height, header_entries)
164175
} else {
165-
self.header_by_blockhash(&prev_blockhash)
166-
.unwrap_or_else(|| panic!("{} is not part of the blockchain", prev_blockhash))
176+
// No new headers, but the new tip could potentially shorten the chain (or be a no-op if it matches the existing tip)
177+
// This should not normally happen, but might due to manual `invalidateblock`
178+
let new_height = self
179+
.header_by_blockhash(new_tip)
180+
.expect("new tip not in chain")
167181
.height()
168-
+ 1
182+
+ 1;
183+
(new_height, vec![])
169184
};
170-
let header_entries = (new_height..)
171-
.zip(hashed_headers.into_iter())
172-
.map(|(height, hashed_header)| HeaderEntry {
173-
height,
174-
hash: hashed_header.blockhash,
175-
header: hashed_header.header,
176-
})
177-
.collect();
178185
let reorged_since = (new_height < self.len()).then_some(new_height);
179186
(header_entries, reorged_since)
180187
}
@@ -200,12 +207,9 @@ impl HeaderList {
200207
#[trace]
201208
pub fn append(&mut self, new_headers: Vec<HeaderEntry>) {
202209
// new_headers[i] -> new_headers[i - 1] (i.e. new_headers.last() is the tip)
203-
for i in 1..new_headers.len() {
204-
assert_eq!(new_headers[i - 1].height() + 1, new_headers[i].height());
205-
assert_eq!(
206-
*new_headers[i - 1].hash(),
207-
new_headers[i].header().prev_blockhash
208-
);
210+
for (curr_header, next_header) in new_headers.iter().tuple_windows() {
211+
assert_eq!(curr_header.height() + 1, next_header.height());
212+
assert_eq!(*curr_header.hash(), next_header.header().prev_blockhash);
209213
}
210214
let new_height = match new_headers.first() {
211215
Some(entry) => {

tests/rest.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,19 @@ fn test_rest() -> Result<()> {
399399
let c_spends = get_outspend(&tx_b.input[0].previous_output)?;
400400
assert_eq!(c_spends["spent"].as_bool(), Some(false));
401401

402+
// Invalidate the tip with no replacement, shortening the chain by one block
403+
tester.invalidate_block(&tester.get_best_block_hash()?)?;
404+
tester.sync()?;
405+
assert_eq!(
406+
get_plain("/blocks/tip/height")?,
407+
(init_height + 20).to_string()
408+
);
409+
402410
// Reorg everything back to genesis
403411
tester.invalidate_block(&tester.get_block_hash(1)?)?;
404-
tester.call::<Value>(
405-
"generateblock",
406-
&[miner_address.to_string().into(), Vec::<Value>::new().into()],
407-
)?;
408412
tester.sync()?;
409413

410-
assert_eq!(get_plain("/blocks/tip/height")?, 1.to_string());
414+
assert_eq!(get_plain("/blocks/tip/height")?, 0.to_string());
411415
assert_eq!(
412416
get_chain_stats(&address)?["funded_txo_sum"].as_u64(),
413417
Some(0)

0 commit comments

Comments
 (0)