Skip to content

Commit 05c406d

Browse files
committed
Merge branch 'fix/undo-records-near-tip-during-ibd'
2 parents a080352 + 415c051 commit 05c406d

7 files changed

Lines changed: 286 additions & 25 deletions

File tree

‎src/server/mod.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub(crate) use subscription::SubscriptionEvent;
4141
const DEFAULT_MAX_TXS_SEEN: usize = 100;
4242
const DEFAULT_MAX_ACTIVE_SUBSCRIPTIONS: usize = 5_000;
4343
const DEFAULT_MAX_SCRIPTS_PER_SUBSCRIPTION: usize = 2_000;
44+
const DEFAULT_REORG_DATA_KEEP_HEIGHTS: u32 = 6;
4445
const PERIODIC_LOGGING_INTERVAL: Duration = Duration::from_secs(300);
4546
const INITIAL_BACKEND_RETRY_DELAY: Duration = Duration::from_secs(1);
4647
const MAX_BACKEND_RETRY_DELAY: Duration = Duration::from_secs(30);
@@ -178,6 +179,7 @@ pub struct Arguments {
178179
pub mempool_sleep_between_cycles_ms: Option<u64>,
179180

180181
/// Number of recent block heights to keep reorg data for. Older reorg data is automatically deleted. Default is 6.
182+
/// Reorg data is not written during initial block download, except for blocks within this many heights of the node tip.
181183
#[cfg(feature = "db")]
182184
#[arg(env, long)]
183185
pub reorg_data_keep_heights: Option<u32>,
@@ -490,7 +492,8 @@ fn get_store(args: &Arguments) -> Result<AnyStore, Error> {
490492
&path,
491493
args.shared_db_cache_mb,
492494
args.enable_db_statistics,
493-
args.reorg_data_keep_heights.unwrap_or(6),
495+
args.reorg_data_keep_heights
496+
.unwrap_or(DEFAULT_REORG_DATA_KEEP_HEIGHTS),
494497
)
495498
.map_err(|e| Error::DBOpen(format!("{e:?}")))?;
496499

@@ -582,6 +585,13 @@ pub async fn inner_main(
582585
last_report: Instant::now(),
583586
}));
584587

588+
#[cfg(feature = "db")]
589+
let reorg_data_keep_heights = args
590+
.reorg_data_keep_heights
591+
.unwrap_or(DEFAULT_REORG_DATA_KEEP_HEIGHTS);
592+
#[cfg(not(feature = "db"))]
593+
let reorg_data_keep_heights = DEFAULT_REORG_DATA_KEEP_HEIGHTS;
594+
585595
let h1 = {
586596
let state = state.clone();
587597
let client: Client =
@@ -603,6 +613,7 @@ pub async fn inner_main(
603613
initial_sync_tx,
604614
shutdown_future,
605615
args.logs_rocksdb_stat_every,
616+
reorg_data_keep_heights,
606617
)
607618
.await
608619
})

‎src/store/db.rs‎

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ pub struct DBStore {
4141
/// Whether we are in Initial Block Download mode.
4242
/// During initial block download we skip reorg data writes, since reorgs are
4343
/// extremely unlikely for old blocks and the data is only needed near tip.
44+
///
45+
/// Every process start begins in IBD mode, so the blocks task turns it off as soon as
46+
/// the block being indexed is within `reorg_data_keep_heights` of the node tip, not
47+
/// only when the tip is reached: a block indexed without reorg data can never be
48+
/// rolled back, and the blocks indexed while catching up after a restart are exactly
49+
/// the ones a reorg is most likely to hit.
4450
ibd: AtomicBool,
4551

4652
/// Number of recent block heights to keep reorg data for. Older reorg data is automatically deleted.
@@ -687,7 +693,10 @@ impl Store for DBStore {
687693

688694
// Store reorg data for potential blockchain reorganization correction
689695
// Skip during IBD (Initial Block Download) as reorgs are extremely unlikely for old blocks
690-
// and this saves significant write overhead during initial sync
696+
// and this saves significant write overhead during initial sync.
697+
// A block written here without reorg data cannot be rolled back later, so the
698+
// caller must call `ibd_finished` before indexing any block that is within
699+
// `reorg_data_keep_heights` of the node tip (see `threads::blocks`).
691700
if !self.ibd.load(Ordering::Relaxed) {
692701
// Create ReorgData and persist it to the database
693702
let reorg_data = ReorgData {
@@ -724,8 +733,9 @@ impl Store for DBStore {
724733
}
725734

726735
fn ibd_finished(&self) {
727-
log::info!("Initial block download finished, enabling reorg data writes");
728-
self.ibd.store(false, Ordering::Relaxed);
736+
if self.ibd.swap(false, Ordering::Relaxed) {
737+
log::info!("Initial block download finished, enabling reorg data writes");
738+
}
729739
}
730740
}
731741

@@ -814,19 +824,22 @@ fn concat_merge(
814824
mod test {
815825
use elements::{hashes::Hash, BlockHash, Txid};
816826
use rocksdb::DB;
817-
use std::{collections::BTreeMap, sync::atomic::AtomicBool};
827+
use std::{
828+
collections::BTreeMap,
829+
sync::atomic::{AtomicBool, Ordering},
830+
};
818831

819832
use crate::store::{
820833
db::{
821834
estimate_history_size, get_or_init_salt, serialize_outpoint, vec_tx_seen_from_be_bytes,
822835
vec_tx_seen_to_be_bytes, TxSeen,
823836
},
824-
Store,
837+
BlockMeta, Store,
825838
};
826839
use crate::OutPoint;
827840
use crate::V;
828841

829-
use super::DBStore;
842+
use super::{DBStore, REORG_CF};
830843

831844
#[test]
832845
fn test_db_hash_compatibility() {
@@ -911,6 +924,58 @@ mod test {
911924
// assert_eq!(r, vec![BlockHash::all_zeros(); 3]);
912925
}
913926

927+
#[test]
928+
fn test_reorg_data_written_only_after_ibd_finished() {
929+
let tempdir = tempfile::TempDir::new().unwrap();
930+
let db = DBStore::open(tempdir.path(), 64, false, 6).unwrap();
931+
let reorg_cf = db.db.cf_handle(REORG_CF).unwrap();
932+
let txid = crate::be::Txid::all_zeros();
933+
934+
let block = |height: u32| BlockMeta::new(height, BlockHash::all_zeros(), height);
935+
let created = |vout: u32| {
936+
let mut m = BTreeMap::new();
937+
m.insert(OutPoint::new(txid, vout), 1u64);
938+
m
939+
};
940+
941+
// While in IBD the block is indexed but no reorg data is persisted...
942+
db.update(&block(1), vec![], BTreeMap::new(), created(1))
943+
.unwrap();
944+
assert!(db
945+
.db
946+
.get_cf(&reorg_cf, 1u32.to_be_bytes())
947+
.unwrap()
948+
.is_none());
949+
// ...so that block can never be rolled back.
950+
let err = db._reorg(1).unwrap_err().to_string();
951+
assert!(err.contains("No reorg data found for height 1"), "{err}");
952+
953+
// Once IBD is finished every indexed block gets reorg data.
954+
db.ibd_finished();
955+
db.update(&block(2), vec![], BTreeMap::new(), created(2))
956+
.unwrap();
957+
assert!(db
958+
.db
959+
.get_cf(&reorg_cf, 2u32.to_be_bytes())
960+
.unwrap()
961+
.is_some());
962+
assert_eq!(
963+
db.get_utxos(&[OutPoint::new(txid, 2)]).unwrap(),
964+
vec![Some(1)]
965+
);
966+
967+
// Calling it again is a no-op, and the block can be rolled back.
968+
db.ibd_finished();
969+
assert!(!db.ibd.load(Ordering::Relaxed));
970+
db.reorg(2);
971+
assert!(db
972+
.db
973+
.get_cf(&reorg_cf, 2u32.to_be_bytes())
974+
.unwrap()
975+
.is_none());
976+
assert_eq!(db.get_utxos(&[OutPoint::new(txid, 2)]).unwrap(), vec![None]);
977+
}
978+
914979
#[test]
915980
fn test_static_txseen_round_trip() {
916981
let txseen = TxSeen::new(crate::be::Txid::all_zeros(), 0, V::Undefined);

‎src/store/mod.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ pub trait Store {
5858
/// height: the height of the block that was reorged (needs to be rolled back)
5959
fn reorg(&self, height: Height);
6060

61-
/// Called when the initial block download is finished
61+
/// Enable reorg data writes for every block indexed from now on.
62+
///
63+
/// Called by the blocks task as soon as the block being indexed is within the reorg
64+
/// retention window of the node tip, and again when the tip is reached. Idempotent.
6265
fn ibd_finished(&self);
6366
}
6467

‎src/test_env.rs‎

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,26 @@ async fn inner_launch_with_node_no_generate(
171171
}
172172
}
173173

174+
/// Generate `block_num` blocks on `node` to a new wallet address, without any server involved.
175+
/// Useful for tests that need to mine blocks while no server is running.
176+
pub fn generate_blocks(node: &BitcoinD, block_num: u32) -> Vec<BlockHash> {
177+
let address: Value = node
178+
.client
179+
.call("getnewaddress", &["label".into(), "p2sh-segwit".into()])
180+
.unwrap();
181+
let result = node
182+
.client
183+
.call::<Value>("generatetoaddress", &[block_num.into(), address])
184+
.unwrap();
185+
186+
result
187+
.as_array()
188+
.unwrap()
189+
.iter()
190+
.map(|v| BlockHash::from_str(v.as_str().unwrap()).unwrap())
191+
.collect()
192+
}
193+
174194
pub fn launch_bitcoin<S: AsRef<OsStr>>(exe: S) -> BitcoinD {
175195
let mut conf = Conf::default();
176196
conf.args = vec!["-regtest", "-fallbackfee=0.0001", "-rest=1", "-txindex=1"];
@@ -245,6 +265,25 @@ impl TestEnv {
245265
// BitcoinD will be automatically dropped here, calling its Drop implementation
246266
}
247267

268+
/// Shut down the server and wait for it to exit, keeping the node alive so that a new
269+
/// server can be started against it, for example on the same database.
270+
pub async fn shutdown_server(self) -> BitcoinD {
271+
let TestEnv {
272+
node,
273+
handle,
274+
tx,
275+
client,
276+
..
277+
} = self;
278+
// Close idle keep-alive connections first: every connection task holds the shared
279+
// state, and with it the database, until the connection is closed.
280+
drop(client);
281+
tx.send(()).unwrap();
282+
let _ = handle.await.unwrap();
283+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
284+
node
285+
}
286+
248287
/// Extract the BitcoinD node from this TestEnv, signaling the server to shutdown.
249288
/// Used in tests that need to restart the server with the same node.
250289
pub fn into_node(self) -> BitcoinD {
@@ -300,22 +339,7 @@ impl TestEnv {
300339
/// Generate blocks on the node without waiting for waterfalls to index them.
301340
/// Useful for tests that expect the server to crash or be unavailable.
302341
pub fn node_generate_no_wait(&self, block_num: u32) -> Vec<BlockHash> {
303-
let address = self.get_new_address(None);
304-
let result = self
305-
.node
306-
.client
307-
.call::<Value>(
308-
"generatetoaddress",
309-
&[block_num.into(), address.to_string().into()],
310-
)
311-
.unwrap();
312-
313-
result
314-
.as_array()
315-
.unwrap()
316-
.iter()
317-
.map(|v| BlockHash::from_str(v.as_str().unwrap()).unwrap())
318-
.collect()
342+
generate_blocks(&self.node, block_num)
319343
}
320344

321345
/// generate `block_num` blocks and wait the waterfalls server had indexed them

0 commit comments

Comments
 (0)