From 7da2f686301a18b29362ccb4d7a293ee91bee843 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 12 Sep 2025 15:32:37 +0200 Subject: [PATCH 01/17] XC-484: fix incomplete doc. --- rs/bitcoin/adapter/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/bitcoin/adapter/src/lib.rs b/rs/bitcoin/adapter/src/lib.rs index 4a87bcae66b5..292ea6e2f743 100644 --- a/rs/bitcoin/adapter/src/lib.rs +++ b/rs/bitcoin/adapter/src/lib.rs @@ -81,7 +81,7 @@ enum ProcessNetworkMessageError { InvalidMessage, } -/// This enum is used to represent errors that +/// Error returned by `Channel::send`. #[derive(Debug)] enum ChannelError {} From 4633874647bf546effb6d4172d5b57c419b4e88f Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 12 Sep 2025 16:34:16 +0200 Subject: [PATCH 02/17] XC-484: add Dogecoin block headers --- MODULE.bazel | 8 ++++++++ rs/bitcoin/adapter/BUILD.bazel | 2 ++ 2 files changed, 10 insertions(+) diff --git a/MODULE.bazel b/MODULE.bazel index ae75648f3395..eb8ccfef463f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -541,6 +541,14 @@ http_file( url = "https://download.dfinity.systems/testdata/mainnet_headers_800k.json.gz", ) +# Contains the first 800_000 headers of the Dogecoin mainnet blockchain with auxiliary proof-of-work. +http_file( + name = "doge_headers_800k_mainnet_auxpow", + downloaded_file_path = "doge_headers_800k_mainnet_auxpow.json.gz", + sha256 = "e138d7c59d237d0eea70943b60038b8755d9aa1f04bc94f315a74775cf8bfc2d", + url = "http://download.dfinity.systems/testdata/doge/doge_headers_800k_mainnet_auxpow.json.gz", +) + # Contains blocks 350_990 to 350_999 (inclusive) of the Bitcoin mainnet blockchain. http_file( name = "bitcoin_adapter_mainnet_blocks", diff --git a/rs/bitcoin/adapter/BUILD.bazel b/rs/bitcoin/adapter/BUILD.bazel index 2ebf4ab05872..e21380da2eba 100644 --- a/rs/bitcoin/adapter/BUILD.bazel +++ b/rs/bitcoin/adapter/BUILD.bazel @@ -162,9 +162,11 @@ rust_bench( data = [ # Keep sorted. "@bitcoin_adapter_mainnet_headers//file", + "@doge_headers_800k_mainnet_auxpow//file", ], env = { "BITCOIN_MAINNET_HEADERS_DATA_PATH": "$(rootpath @bitcoin_adapter_mainnet_headers//file)", + "DOGECOIN_MAINNET_HEADERS_DATA_PATH": "$(rootpath @doge_headers_800k_mainnet_auxpow//file)", }, deps = [ # Keep sorted. From 09620bd147b086f95365c2de2de5c3272866d695 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 12 Sep 2025 16:46:24 +0200 Subject: [PATCH 03/17] XC-484: bench Dogecoin --- rs/bitcoin/adapter/benches/e2e.rs | 49 +++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 30c914ce79f1..01cd34ccaee6 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -1,4 +1,4 @@ -use bitcoin::{block::Header as BlockHeader, BlockHash, Network}; +use bitcoin::{block::Header as BlockHeader, BlockHash}; use criterion::measurement::Measurement; use criterion::{criterion_group, criterion_main, BenchmarkGroup, Criterion}; use ic_btc_adapter::{ @@ -66,7 +66,7 @@ fn prepare( } fn e2e(criterion: &mut Criterion) { - let network = Network::Regtest; + let network = bitcoin::Network::Regtest; let mut config = Config::default_with(network.into()); let mut processed_block_hashes = vec![]; @@ -185,19 +185,50 @@ fn add_800k_block_headers(criterion: &mut Criterion) { ); retrieve_headers::(&headers_data_path) }); - // Call BITCOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation). + static DOGECOIN_HEADERS: LazyLock> = LazyLock::new(|| { + let headers_data_path = PathBuf::from( + std::env::var("DOGECOIN_MAINNET_HEADERS_DATA_PATH") + .expect("Failed to get test data path env variable"), + ); + retrieve_headers::(&headers_data_path) + }); + { + // warm-up: Call BITCOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation). + // Genesis block header is automatically added when instantiating BlockchainState + let bitcoin_headers_to_add = &BITCOIN_HEADERS.as_slice()[1..]; + assert_eq!(bitcoin_headers_to_add.len(), 800_000); + let mut group = criterion.benchmark_group("bitcoin_800k"); + group.sample_size(10); + + group.bench_function("add_headers", |bench| { + bench.iter(|| { + let mut blockchain_state = + BlockchainState::new(bitcoin::Network::Bitcoin, &MetricsRegistry::default()); + // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries + for chunk in bitcoin_headers_to_add.chunks(MAX_HEADERS_SIZE) { + let (added_headers, error) = blockchain_state.add_headers(chunk); + assert_eq!(error, None); + assert_eq!(added_headers.len(), chunk.len()) + } + }) + }); + } + + // warm-up: Call DOGECOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation). // Genesis block header is automatically added when instantiating BlockchainState - let bitcoin_headers_to_add = &BITCOIN_HEADERS.as_slice()[1..]; - assert_eq!(bitcoin_headers_to_add.len(), 800_000); - let mut group = criterion.benchmark_group("bitcoin_800k"); + let dogecoin_headers_to_add = &DOGECOIN_HEADERS.as_slice()[1..]; + assert_eq!(dogecoin_headers_to_add.len(), 800_000); + let mut group = criterion.benchmark_group("dogecoin_800k"); group.sample_size(10); group.bench_function("add_headers", |bench| { bench.iter(|| { - let mut blockchain_state = - BlockchainState::new(Network::Bitcoin, &MetricsRegistry::default()); + let mut blockchain_state = BlockchainState::new( + bitcoin::dogecoin::Network::Dogecoin, + &MetricsRegistry::default(), + ); // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries - for chunk in bitcoin_headers_to_add.chunks(MAX_HEADERS_SIZE) { + for chunk in dogecoin_headers_to_add.chunks(MAX_HEADERS_SIZE) { let (added_headers, error) = blockchain_state.add_headers(chunk); assert_eq!(error, None); assert_eq!(added_headers.len(), chunk.len()) From ac007319cddfbceceb7de81b4c54002a889dff85 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 12 Sep 2025 17:39:12 +0200 Subject: [PATCH 04/17] XC-484: refactor --- rs/bitcoin/adapter/benches/e2e.rs | 88 ++++++++++++++----------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 01cd34ccaee6..5a628de1d653 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -17,8 +17,8 @@ use ic_logger::replica_logger::no_op_logger; use ic_metrics::MetricsRegistry; use rand::{CryptoRng, Rng}; use sha2::Digest; +use std::fmt; use std::path::{Path, PathBuf}; -use std::sync::LazyLock; use tempfile::Builder; type BitcoinAdapterClient = Box< @@ -177,58 +177,52 @@ fn random_header(rng: &mut R) -> [u8; N] { header } -fn add_800k_block_headers(criterion: &mut Criterion) { - static BITCOIN_HEADERS: LazyLock> = LazyLock::new(|| { - let headers_data_path = PathBuf::from( - std::env::var("BITCOIN_MAINNET_HEADERS_DATA_PATH") - .expect("Failed to get test data path env variable"), - ); - retrieve_headers::(&headers_data_path) - }); - static DOGECOIN_HEADERS: LazyLock> = LazyLock::new(|| { - let headers_data_path = PathBuf::from( - std::env::var("DOGECOIN_MAINNET_HEADERS_DATA_PATH") - .expect("Failed to get test data path env variable"), - ); - retrieve_headers::(&headers_data_path) - }); - { - // warm-up: Call BITCOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation). - // Genesis block header is automatically added when instantiating BlockchainState - let bitcoin_headers_to_add = &BITCOIN_HEADERS.as_slice()[1..]; - assert_eq!(bitcoin_headers_to_add.len(), 800_000); - let mut group = criterion.benchmark_group("bitcoin_800k"); - group.sample_size(10); - - group.bench_function("add_headers", |bench| { - bench.iter(|| { - let mut blockchain_state = - BlockchainState::new(bitcoin::Network::Bitcoin, &MetricsRegistry::default()); - // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries - for chunk in bitcoin_headers_to_add.chunks(MAX_HEADERS_SIZE) { - let (added_headers, error) = blockchain_state.add_headers(chunk); - assert_eq!(error, None); - assert_eq!(added_headers.len(), chunk.len()) - } - }) - }); - } +fn add_block_headers(criterion: &mut Criterion) { + add_block_headers_for( + criterion, + bitcoin::Network::Bitcoin, + "BITCOIN_MAINNET_HEADERS_DATA_PATH", + 800_000, + ); + add_block_headers_for( + criterion, + bitcoin::dogecoin::Network::Dogecoin, + "DOGECOIN_MAINNET_HEADERS_DATA_PATH", + 800_000, + ); +} - // warm-up: Call DOGECOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation). +fn add_block_headers_for( + criterion: &mut Criterion, + network: Network, + headers_data_env: &str, + expected_num_headers_to_add: usize, +) where + Network::Header: for<'de> serde::Deserialize<'de>, +{ + let headers_data_path = PathBuf::from( + std::env::var(headers_data_env).expect("Failed to get test data path env variable"), + ); + let headers = retrieve_headers::(&headers_data_path); // Genesis block header is automatically added when instantiating BlockchainState - let dogecoin_headers_to_add = &DOGECOIN_HEADERS.as_slice()[1..]; - assert_eq!(dogecoin_headers_to_add.len(), 800_000); - let mut group = criterion.benchmark_group("dogecoin_800k"); + let headers_to_add = &headers.as_slice()[1..]; + assert_eq!(headers_to_add.len(), expected_num_headers_to_add); + let mut group = criterion.benchmark_group(format!("{network}_{expected_num_headers_to_add}")); group.sample_size(10); + bench_add_headers(&mut group, network, headers_to_add); +} + +fn bench_add_headers( + group: &mut BenchmarkGroup<'_, M>, + network: Network, + headers: &[Network::Header], +) { group.bench_function("add_headers", |bench| { bench.iter(|| { - let mut blockchain_state = BlockchainState::new( - bitcoin::dogecoin::Network::Dogecoin, - &MetricsRegistry::default(), - ); + let mut blockchain_state = BlockchainState::new(network, &MetricsRegistry::default()); // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries - for chunk in dogecoin_headers_to_add.chunks(MAX_HEADERS_SIZE) { + for chunk in headers.chunks(MAX_HEADERS_SIZE) { let (added_headers, error) = blockchain_state.add_headers(chunk); assert_eq!(error, None); assert_eq!(added_headers.len(), chunk.len()) @@ -267,7 +261,7 @@ fn decompress>(location: P) -> Vec { // the request as being processed, with the aim to receive the last 25 blocks of each fork. // Performance metrics are captured from the sending of the deserialised request through // to receiving the response and its deserialisation. -criterion_group!(benches, e2e, hash_block_header, add_800k_block_headers); +criterion_group!(benches, e2e, hash_block_header, add_block_headers); // The benchmark can be run using: // bazel run //rs/bitcoin/adapter:e2e_bench From 9e9d24da8a911d49cc0eb5b2f770d350e6854b34 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 16 Sep 2025 07:19:16 +0200 Subject: [PATCH 05/17] XC-484: fix merge --- rs/bitcoin/adapter/benches/e2e.rs | 2 +- rs/bitcoin/adapter/src/header_cache.rs | 2 +- rs/bitcoin/validation/src/header.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index ffe68bd33760..80b0b8ace56c 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -1,4 +1,4 @@ -use bitcoin::{block::Header as BlockHeader, BlockHash }; +use bitcoin::{BlockHash, block::Header as BlockHeader}; use criterion::measurement::Measurement; use criterion::{BenchmarkGroup, Criterion, criterion_group, criterion_main}; use ic_btc_adapter::{ diff --git a/rs/bitcoin/adapter/src/header_cache.rs b/rs/bitcoin/adapter/src/header_cache.rs index 5056b5aaa8dd..aa209e588707 100644 --- a/rs/bitcoin/adapter/src/header_cache.rs +++ b/rs/bitcoin/adapter/src/header_cache.rs @@ -87,7 +87,7 @@ pub enum AddHeaderResult { HeaderAlreadyExists, } -#[derive(Debug, Error)] +#[derive(Debug, Eq, PartialEq, Error)] pub enum AddHeaderError { /// When the received header is invalid (eg: not of the right format). #[error("Received an invalid block header: {0}")] diff --git a/rs/bitcoin/validation/src/header.rs b/rs/bitcoin/validation/src/header.rs index d1805d9cb851..6df3da84a09c 100644 --- a/rs/bitcoin/validation/src/header.rs +++ b/rs/bitcoin/validation/src/header.rs @@ -11,7 +11,7 @@ use crate::{ }; /// An error thrown when trying to validate a header. -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum ValidateHeaderError { /// Used when the timestamp in the header is lower than /// the median of timestamps of past 11 headers. From c7a9a79f6ee09325028c09401f1232d1a4d19939 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 17 Sep 2025 12:09:31 +0200 Subject: [PATCH 06/17] XC-484: bench lmdb --- rs/bitcoin/adapter/benches/e2e.rs | 39 ++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 80b0b8ace56c..22e40ae618fe 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -1,6 +1,6 @@ use bitcoin::{BlockHash, block::Header as BlockHeader}; use criterion::measurement::Measurement; -use criterion::{BenchmarkGroup, Criterion, criterion_group, criterion_main}; +use criterion::{BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main}; use ic_btc_adapter::{ BlockchainNetwork, BlockchainState, Config, IncomingSource, MAX_HEADERS_SIZE, start_server, }; @@ -19,7 +19,7 @@ use rand::{CryptoRng, Rng}; use sha2::Digest; use std::fmt; use std::path::{Path, PathBuf}; -use tempfile::Builder; +use tempfile::{Builder, tempdir}; type BitcoinAdapterClient = Box< dyn RpcAdapterClient, @@ -218,15 +218,36 @@ fn bench_add_headers( network: Network, headers: &[Network::Header], ) { - group.bench_function("add_headers", |bench| { + fn add_headers( + blockchain_state: &mut BlockchainState, + network: Network, + headers: &[Network::Header], + ) { + // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries + for chunk in headers.chunks(MAX_HEADERS_SIZE) { + let (added_headers, error) = blockchain_state.add_headers(chunk); + assert_eq!(error, None); + assert_eq!(added_headers.len(), chunk.len()) + } + } + + group.bench_function(BenchmarkId::new("add_headers", "in_memory"), |bench| { bench.iter(|| { let mut blockchain_state = BlockchainState::new(network, &MetricsRegistry::default()); - // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries - for chunk in headers.chunks(MAX_HEADERS_SIZE) { - let (added_headers, error) = blockchain_state.add_headers(chunk); - assert_eq!(error, None); - assert_eq!(added_headers.len(), chunk.len()) - } + add_headers(&mut blockchain_state, network, headers); + }) + }); + + group.bench_function(BenchmarkId::new("add_headers", "lmdb"), |bench| { + bench.iter(|| { + let dir = tempdir().unwrap(); + let mut blockchain_state = BlockchainState::new_with_cache_dir( + network, + dir.path().to_path_buf(), + &MetricsRegistry::default(), + no_op_logger(), + ); + add_headers(&mut blockchain_state, network, headers); }) }); } From 3f6fae73811e40277023eb7f2dec7ade3fc3fc0b Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 17 Sep 2025 12:09:37 +0200 Subject: [PATCH 07/17] XC-484: move comment --- rs/bitcoin/adapter/benches/e2e.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 22e40ae618fe..64c873b54770 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -65,6 +65,11 @@ fn prepare( } } +// This simulation constructs a blockchain comprising four forks, each of 2000 blocks. +// For an extended BFS execution, the initial 1975 blocks of every branch are marked in +// the request as being processed, with the aim to receive the last 25 blocks of each fork. +// Performance metrics are captured from the sending of the deserialised request through +// to receiving the response and its deserialisation. fn e2e(criterion: &mut Criterion) { let network = bitcoin::Network::Regtest; let mut config = Config::default_with(network.into()); @@ -277,11 +282,6 @@ fn decompress>(location: P) -> Vec { decompressed } -// This simulation constructs a blockchain comprising four forks, each of 2000 blocks. -// For an extended BFS execution, the initial 1975 blocks of every branch are marked in -// the request as being processed, with the aim to receive the last 25 blocks of each fork. -// Performance metrics are captured from the sending of the deserialised request through -// to receiving the response and its deserialisation. criterion_group!(benches, e2e, hash_block_header, add_block_headers); // The benchmark can be run using: From 16aa636ce68485c78af032076c0f447a8cab5645 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 18 Sep 2025 07:40:26 +0200 Subject: [PATCH 08/17] XC-484: fix merge --- rs/bitcoin/adapter/benches/e2e.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index fa016cbd7364..f863b7db7b7f 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -2,7 +2,8 @@ use bitcoin::{BlockHash, block::Header as BlockHeader}; use criterion::measurement::Measurement; use criterion::{BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main}; use ic_btc_adapter::{ - BlockchainNetwork, BlockchainState, Config, IncomingSource, MAX_HEADERS_SIZE, start_server, + BlockchainNetwork, BlockchainState, Config, HeaderValidator, IncomingSource, MAX_HEADERS_SIZE, + start_server, }; use ic_btc_adapter_client::setup_bitcoin_adapter_clients; use ic_btc_adapter_test_utils::generate_headers; @@ -198,6 +199,7 @@ fn add_block_headers_for( expected_num_headers_to_add: usize, ) where Network::Header: for<'de> serde::Deserialize<'de>, + BlockchainState: HeaderValidator, { let headers_data_path = PathBuf::from( std::env::var(headers_data_env).expect("Failed to get test data path env variable"), @@ -216,16 +218,20 @@ fn bench_add_headers( group: &mut BenchmarkGroup<'_, M>, network: Network, headers: &[Network::Header], -) { +) where + BlockchainState: HeaderValidator, +{ fn add_headers( blockchain_state: &mut BlockchainState, - network: Network, headers: &[Network::Header], - ) { + handle: &tokio::runtime::Handle, + ) where + BlockchainState: HeaderValidator, + { // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries for chunk in headers.chunks(MAX_HEADERS_SIZE) { let (added_headers, error) = - rt.block_on(async { blockchain_state.add_headers(chunk).await }); + handle.block_on(async { blockchain_state.add_headers(chunk).await }); assert!(error.is_none(), "Failed to add headers: {}", error.unwrap()); assert_eq!(added_headers.len(), chunk.len()) } @@ -236,7 +242,7 @@ fn bench_add_headers( group.bench_function(BenchmarkId::new("add_headers", "in_memory"), |bench| { bench.iter(|| { let mut blockchain_state = BlockchainState::new(network, &MetricsRegistry::default()); - add_headers(&mut blockchain_state, network, headers); + add_headers(&mut blockchain_state, headers, rt.handle()); }) }); @@ -249,7 +255,7 @@ fn bench_add_headers( &MetricsRegistry::default(), no_op_logger(), ); - add_headers(&mut blockchain_state, network, headers); + add_headers(&mut blockchain_state, headers, rt.handle()); }) }); } From 6d54f574015463ec66ad488df51f5391ff74d8cd Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 18 Sep 2025 07:40:34 +0200 Subject: [PATCH 09/17] XC-484: adapt sample size --- rs/bitcoin/adapter/benches/e2e.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index f863b7db7b7f..ba1a7c5743d7 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -209,7 +209,7 @@ fn add_block_headers_for( let headers_to_add = &headers.as_slice()[1..]; assert_eq!(headers_to_add.len(), expected_num_headers_to_add); let mut group = criterion.benchmark_group(format!("{network}_{expected_num_headers_to_add}")); - group.sample_size(10); + group.sample_size(1); bench_add_headers(&mut group, network, headers_to_add); } From f0de817217abf3b1ecd7166f74f65d1dd9046bb2 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 18 Sep 2025 08:49:31 +0200 Subject: [PATCH 10/17] XC-484: fix sample size and runtime --- rs/bitcoin/adapter/benches/e2e.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index ba1a7c5743d7..df613a63b36a 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -209,7 +209,7 @@ fn add_block_headers_for( let headers_to_add = &headers.as_slice()[1..]; assert_eq!(headers_to_add.len(), expected_num_headers_to_add); let mut group = criterion.benchmark_group(format!("{network}_{expected_num_headers_to_add}")); - group.sample_size(1); + group.sample_size(10); bench_add_headers(&mut group, network, headers_to_add); } @@ -224,14 +224,14 @@ fn bench_add_headers( fn add_headers( blockchain_state: &mut BlockchainState, headers: &[Network::Header], - handle: &tokio::runtime::Handle, + runtime: &tokio::runtime::Runtime, ) where BlockchainState: HeaderValidator, { // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries for chunk in headers.chunks(MAX_HEADERS_SIZE) { let (added_headers, error) = - handle.block_on(async { blockchain_state.add_headers(chunk).await }); + runtime.block_on(async { blockchain_state.add_headers(chunk).await }); assert!(error.is_none(), "Failed to add headers: {}", error.unwrap()); assert_eq!(added_headers.len(), chunk.len()) } @@ -242,7 +242,7 @@ fn bench_add_headers( group.bench_function(BenchmarkId::new("add_headers", "in_memory"), |bench| { bench.iter(|| { let mut blockchain_state = BlockchainState::new(network, &MetricsRegistry::default()); - add_headers(&mut blockchain_state, headers, rt.handle()); + add_headers(&mut blockchain_state, headers, &rt); }) }); @@ -255,7 +255,7 @@ fn bench_add_headers( &MetricsRegistry::default(), no_op_logger(), ); - add_headers(&mut blockchain_state, headers, rt.handle()); + add_headers(&mut blockchain_state, headers, &rt); }) }); } From 6346fb2d6c55252861a8120bb2988beb0298eddc Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 23 Sep 2025 16:57:03 +0200 Subject: [PATCH 11/17] XC-484: fix merge --- rs/bitcoin/adapter/benches/e2e.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 60711b2209ae..9b0f3788baaa 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -241,7 +241,8 @@ fn bench_add_headers( group.bench_function(BenchmarkId::new("add_headers", "in_memory"), |bench| { bench.iter(|| { - let mut blockchain_state = BlockchainState::new(network, &MetricsRegistry::default()); + let mut blockchain_state = + BlockchainState::new(network, None, &MetricsRegistry::default(), no_op_logger()); add_headers(&mut blockchain_state, headers, &rt); }) }); @@ -249,9 +250,9 @@ fn bench_add_headers( group.bench_function(BenchmarkId::new("add_headers", "lmdb"), |bench| { bench.iter(|| { let dir = tempdir().unwrap(); - let mut blockchain_state = BlockchainState::new_with_cache_dir( + let mut blockchain_state = BlockchainState::new( network, - dir.path().to_path_buf(), + Some(dir.path().to_path_buf()), &MetricsRegistry::default(), no_op_logger(), ); From 0d1f5f243321fbb62d750b52c2cd2a28436d70bf Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 24 Sep 2025 10:02:30 +0200 Subject: [PATCH 12/17] XC-484: purge headers --- rs/bitcoin/adapter/benches/e2e.rs | 10 +++++-- rs/bitcoin/adapter/src/blockchainstate.rs | 25 +++++++++++++++++ rs/bitcoin/adapter/src/header_cache.rs | 34 +++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 9b0f3788baaa..f235cdeee587 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -2,8 +2,8 @@ use bitcoin::{BlockHash, block::Header as BlockHeader}; use criterion::measurement::Measurement; use criterion::{BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main}; use ic_btc_adapter::{ - BlockchainNetwork, BlockchainState, Config, HeaderValidator, IncomingSource, MAX_HEADERS_SIZE, - start_server, + BlockchainHeader, BlockchainNetwork, BlockchainState, Config, HeaderValidator, IncomingSource, + MAX_HEADERS_SIZE, start_server, }; use ic_btc_adapter_client::setup_bitcoin_adapter_clients; use ic_btc_adapter_test_utils::generate_headers; @@ -257,6 +257,12 @@ fn bench_add_headers( no_op_logger(), ); add_headers(&mut blockchain_state, headers, &rt); + rt.block_on(async { + blockchain_state + .persist_and_prune_headers_below_anchor(headers.last().unwrap().block_hash()) + .await + }); + assert_eq!(blockchain_state.num_headers(), Ok((headers.len(), 1))); }) }); } diff --git a/rs/bitcoin/adapter/src/blockchainstate.rs b/rs/bitcoin/adapter/src/blockchainstate.rs index 7ffbf3953041..e3a4481ed637 100644 --- a/rs/bitcoin/adapter/src/blockchainstate.rs +++ b/rs/bitcoin/adapter/src/blockchainstate.rs @@ -206,6 +206,19 @@ where Ok(result) } + /// Background task to ersist headers below the anchor (as headers) and the anchor (as tip) on to disk, and + /// prune headers below the anchor from the in-memory cache. + pub async fn persist_and_prune_headers_below_anchor( + &self, + anchor: BlockHash, + ) -> tokio::task::JoinHandle<()> { + let header_cache = self.header_cache.clone(); + tokio::task::spawn_blocking(move || { + // Error is ignored, since it is a background task + let _ = header_cache.persist_and_prune_headers_below_anchor(anchor); + }) + } + /// This method adds a new block to the `block_cache` pub async fn add_block( &self, @@ -336,6 +349,18 @@ where .map(|block| block.len()) .sum() } + + /// Number of headers stored. + /// + /// Return a pair where + /// 1. Number of headers stored on disk + /// 2. Number of headers stored in memory + pub fn num_headers(&self) -> Result<(usize, usize), String> { + self.header_cache + .get_num_headers() + // do not expose internal error type + .map_err(|e| e.to_string()) + } } impl HeaderStore for BlockchainState { diff --git a/rs/bitcoin/adapter/src/header_cache.rs b/rs/bitcoin/adapter/src/header_cache.rs index 961e60157f77..010228c2e26b 100644 --- a/rs/bitcoin/adapter/src/header_cache.rs +++ b/rs/bitcoin/adapter/src/header_cache.rs @@ -142,6 +142,9 @@ pub trait HeaderCache: Send + Sync { /// Return the number of tips. fn get_num_tips(&self) -> usize; + /// Return the number of headers. + fn get_num_headers(&self) -> usize; + /// Return the ancestor from the given block hash to the current anchor in the /// in-memory cache as a chain of headers, where each element is the only child /// of the next, and the first element (tip) has no child. @@ -223,6 +226,10 @@ impl HeaderCache for RwLock usize { + self.read().unwrap().cache.len() + } + fn get_ancestor_chain(&self, from: BlockHash) -> Vec<(BlockHash, HeaderNode
)> { let mut hash = from; let mut to_persist = Vec::new(); @@ -358,6 +365,12 @@ impl LMDBHeaderCache { Ok(node) } + fn tx_get_num_headers(&self, tx: &Tx) -> Result { + tx.stat(self.headers) + .map(|stat| stat.entries()) + .map_err(LMDBCacheError::Lmdb) + } + fn tx_add_header( &self, tx: &mut RwTransaction, @@ -496,6 +509,27 @@ impl HybridHeaderCache
}) } + /// Number of headers stored. + /// + /// Return a pair where + /// 1. Number of headers stored on disk + /// 2. Number of headers stored in memory + pub fn get_num_headers(&self) -> Result<(usize, usize), LMDBCacheError> { + let num_headers_in_memory = self.in_memory.get_num_headers(); + if self.on_disk.is_none() { + return Ok((0, num_headers_in_memory)); + } + + let cache = self.on_disk.as_ref().unwrap(); + let num_headers_on_disk = log_err!( + cache.run_ro_txn(|tx| cache.tx_get_num_headers(tx)), + cache.log, + "get_num_headers" + )?; + + Ok((num_headers_on_disk, num_headers_in_memory)) + } + /// Get a header by hash. pub fn get_header(&self, hash: BlockHash) -> Option> { self.in_memory From 16f2316c11b30329000e1f7b02a4c62e986b4d01 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 25 Sep 2025 07:03:11 +0200 Subject: [PATCH 13/17] XC-484: fix await purging --- rs/bitcoin/adapter/benches/e2e.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 5ad641750dd2..4e9b2556b9c6 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -262,8 +262,10 @@ fn bench_add_headers( blockchain_state .persist_and_prune_headers_below_anchor(headers.last().unwrap().block_hash()) .await - }); - assert_eq!(blockchain_state.num_headers(), Ok((headers.len(), 1))); + .await + }) + .unwrap(); + assert_eq!(blockchain_state.num_headers(), Ok((headers.len() + 2, 1))); }) }); } From e26fa9ec8283be6d8978822463372c2823558926 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 25 Sep 2025 15:41:51 +0200 Subject: [PATCH 14/17] XC-484: simplify async in persist_and_prune_headers_below_anchor --- rs/bitcoin/adapter/benches/e2e.rs | 1 - rs/bitcoin/adapter/src/blockchainstate.rs | 2 +- rs/bitcoin/adapter/src/get_successors_handler.rs | 7 ++----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index 4e9b2556b9c6..d3d74324f4c3 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -262,7 +262,6 @@ fn bench_add_headers( blockchain_state .persist_and_prune_headers_below_anchor(headers.last().unwrap().block_hash()) .await - .await }) .unwrap(); assert_eq!(blockchain_state.num_headers(), Ok((headers.len() + 2, 1))); diff --git a/rs/bitcoin/adapter/src/blockchainstate.rs b/rs/bitcoin/adapter/src/blockchainstate.rs index 7d2b7d882b3d..83f9143e4443 100644 --- a/rs/bitcoin/adapter/src/blockchainstate.rs +++ b/rs/bitcoin/adapter/src/blockchainstate.rs @@ -205,7 +205,7 @@ where /// Background task to ersist headers below the anchor (as headers) and the anchor (as tip) on to disk, and /// prune headers below the anchor from the in-memory cache. - pub async fn persist_and_prune_headers_below_anchor( + pub fn persist_and_prune_headers_below_anchor( &self, anchor: BlockHash, ) -> tokio::task::JoinHandle<()> { diff --git a/rs/bitcoin/adapter/src/get_successors_handler.rs b/rs/bitcoin/adapter/src/get_successors_handler.rs index d1afbd36a896..e5ef89481550 100644 --- a/rs/bitcoin/adapter/src/get_successors_handler.rs +++ b/rs/bitcoin/adapter/src/get_successors_handler.rs @@ -127,17 +127,14 @@ impl GetSuccessorsHandler { // Spawn persist-to-disk task without waiting for it to finish, and make sure there // is only one task running at a time. - let cache = self.state.header_cache.clone(); + let state = self.state.clone(); let mut handle = self.pruning_task_handle.lock().unwrap(); let is_finished = handle .as_ref() .map(|handle| handle.is_finished()) .unwrap_or(true); if is_finished { - *handle = Some(tokio::task::spawn_blocking(move || { - // Error is ignored, since it is a background task - let _ = cache.persist_and_prune_headers_below_anchor(request.anchor); - })); + *handle = Some(state.persist_and_prune_headers_below_anchor(request.anchor)); } let (blocks, next, obsolete_blocks) = { From 1a912a2394df193ecaf011f8b5248688c6042faf Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 25 Sep 2025 15:48:02 +0200 Subject: [PATCH 15/17] XC-484: fix one-off error in tx_get_num_headers --- rs/bitcoin/adapter/src/header_cache.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rs/bitcoin/adapter/src/header_cache.rs b/rs/bitcoin/adapter/src/header_cache.rs index e361fb08fecf..42be31999f31 100644 --- a/rs/bitcoin/adapter/src/header_cache.rs +++ b/rs/bitcoin/adapter/src/header_cache.rs @@ -370,9 +370,15 @@ impl LMDBHeaderCache { } fn tx_get_num_headers(&self, tx: &Tx) -> Result { - tx.stat(self.headers) + let num = tx + .stat(self.headers) .map(|stat| stat.entries()) - .map_err(LMDBCacheError::Lmdb) + .map_err(LMDBCacheError::Lmdb)?; + assert!( + num > 0, + "BUG: LMDBHeaderCache::new_with_genesis adds the tip header key '{TIP_KEY}'" + ); + Ok(num - 1) } fn tx_add_header( From eda3c18ec987b4fc47a612c8f9ab9575d9879458 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 25 Sep 2025 15:57:38 +0200 Subject: [PATCH 16/17] XC-484: prune after each chunk --- rs/bitcoin/adapter/benches/e2e.rs | 33 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index d3d74324f4c3..ede729ea9c5f 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -225,16 +225,36 @@ fn bench_add_headers( fn add_headers( blockchain_state: &mut BlockchainState, headers: &[Network::Header], + expect_pruning: bool, runtime: &tokio::runtime::Runtime, ) where BlockchainState: HeaderValidator, { + // Genesis block header is automatically added when instantiating BlockchainState + let mut num_added_headers = 1; // Headers are processed in chunks of at most MAX_HEADERS_SIZE entries for chunk in headers.chunks(MAX_HEADERS_SIZE) { let (added_headers, error) = runtime.block_on(async { blockchain_state.add_headers(chunk).await }); assert!(error.is_none(), "Failed to add headers: {}", error.unwrap()); - assert_eq!(added_headers.len(), chunk.len()) + assert_eq!(added_headers.len(), chunk.len()); + num_added_headers += added_headers.len(); + + runtime + .block_on(async { + blockchain_state + .persist_and_prune_headers_below_anchor(chunk.last().unwrap().block_hash()) + .await + }) + .unwrap(); + let (num_headers_disk, num_headers_memory) = blockchain_state.num_headers().unwrap(); + if expect_pruning { + assert_eq!(num_headers_disk, num_added_headers); + assert_eq!(num_headers_memory, 1); + } else { + assert_eq!(num_headers_disk, 0); + assert_eq!(num_headers_memory, num_added_headers); + } } } @@ -244,7 +264,7 @@ fn bench_add_headers( bench.iter(|| { let mut blockchain_state = BlockchainState::new(network, None, &MetricsRegistry::default(), no_op_logger()); - add_headers(&mut blockchain_state, headers, &rt); + add_headers(&mut blockchain_state, headers, false, &rt); }) }); @@ -257,14 +277,7 @@ fn bench_add_headers( &MetricsRegistry::default(), no_op_logger(), ); - add_headers(&mut blockchain_state, headers, &rt); - rt.block_on(async { - blockchain_state - .persist_and_prune_headers_below_anchor(headers.last().unwrap().block_hash()) - .await - }) - .unwrap(); - assert_eq!(blockchain_state.num_headers(), Ok((headers.len() + 2, 1))); + add_headers(&mut blockchain_state, headers, true, &rt); }) }); } From 1132844d68bf63ca141c9880b4a04aab08997621 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 26 Sep 2025 06:52:42 +0200 Subject: [PATCH 17/17] XC-484: remove unnecessary clone --- rs/bitcoin/adapter/src/get_successors_handler.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rs/bitcoin/adapter/src/get_successors_handler.rs b/rs/bitcoin/adapter/src/get_successors_handler.rs index e5ef89481550..ae1f9c1639dd 100644 --- a/rs/bitcoin/adapter/src/get_successors_handler.rs +++ b/rs/bitcoin/adapter/src/get_successors_handler.rs @@ -127,14 +127,16 @@ impl GetSuccessorsHandler { // Spawn persist-to-disk task without waiting for it to finish, and make sure there // is only one task running at a time. - let state = self.state.clone(); let mut handle = self.pruning_task_handle.lock().unwrap(); let is_finished = handle .as_ref() .map(|handle| handle.is_finished()) .unwrap_or(true); if is_finished { - *handle = Some(state.persist_and_prune_headers_below_anchor(request.anchor)); + *handle = Some( + self.state + .persist_and_prune_headers_below_anchor(request.anchor), + ); } let (blocks, next, obsolete_blocks) = {