Skip to content

Commit 70b3d80

Browse files
authored
perf(dogecoin): benchmark adapter for adding Dogecoin block headers (#6740)
Follow-up on #6706 to benchmark the Dogecoin adapter when adding 800k Dogecoin block headers (with auxiliary proof of work) form mainnet.
1 parent 604ebf8 commit 70b3d80

File tree

8 files changed

+183
-39
lines changed

8 files changed

+183
-39
lines changed

MODULE.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,14 @@ http_file(
541541
url = "https://download.dfinity.systems/testdata/mainnet_headers_800k.json.gz",
542542
)
543543

544+
# Contains the first 800_000 headers of the Dogecoin mainnet blockchain with auxiliary proof-of-work.
545+
http_file(
546+
name = "doge_headers_800k_mainnet_auxpow",
547+
downloaded_file_path = "doge_headers_800k_mainnet_auxpow.json.gz",
548+
sha256 = "e138d7c59d237d0eea70943b60038b8755d9aa1f04bc94f315a74775cf8bfc2d",
549+
url = "http://download.dfinity.systems/testdata/doge/doge_headers_800k_mainnet_auxpow.json.gz",
550+
)
551+
544552
# Contains blocks 350_990 to 350_999 (inclusive) of the Bitcoin mainnet blockchain.
545553
http_file(
546554
name = "bitcoin_adapter_mainnet_blocks",

rs/bitcoin/adapter/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ rust_bench(
164164
data = [
165165
# Keep sorted.
166166
"@bitcoin_adapter_mainnet_headers//file",
167+
"@doge_headers_800k_mainnet_auxpow//file",
167168
],
168169
env = {
169170
"BITCOIN_MAINNET_HEADERS_DATA_PATH": "$(rootpath @bitcoin_adapter_mainnet_headers//file)",
171+
"DOGECOIN_MAINNET_HEADERS_DATA_PATH": "$(rootpath @doge_headers_800k_mainnet_auxpow//file)",
170172
},
171173
deps = [
172174
# Keep sorted.

rs/bitcoin/adapter/benches/e2e.rs

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use bitcoin::{BlockHash, Network, block::Header as BlockHeader};
22
use criterion::measurement::Measurement;
3-
use criterion::{BenchmarkGroup, Criterion, criterion_group, criterion_main};
3+
use criterion::{BenchmarkGroup, BenchmarkId, Criterion, criterion_group, criterion_main};
44
use ic_btc_adapter::{
5-
BlockchainNetwork, BlockchainState, Config, IncomingSource, MAX_HEADERS_SIZE, start_server,
5+
BlockchainHeader, BlockchainNetwork, BlockchainState, Config, HeaderValidator, IncomingSource,
6+
MAX_HEADERS_SIZE, start_server,
67
};
78
use ic_btc_adapter_client::setup_bitcoin_adapter_clients;
89
use ic_btc_adapter_test_utils::generate_headers;
@@ -17,9 +18,9 @@ use ic_logger::replica_logger::no_op_logger;
1718
use ic_metrics::MetricsRegistry;
1819
use rand::{CryptoRng, Rng};
1920
use sha2::Digest;
21+
use std::fmt;
2022
use std::path::{Path, PathBuf};
21-
use std::sync::LazyLock;
22-
use tempfile::Builder;
23+
use tempfile::{Builder, tempdir};
2324

2425
type BitcoinAdapterClient = Box<
2526
dyn RpcAdapterClient<BitcoinAdapterRequestWrapper, Response = BitcoinAdapterResponseWrapper>,
@@ -65,6 +66,11 @@ fn prepare(
6566
}
6667
}
6768

69+
// This simulation constructs a blockchain comprising four forks, each of 2000 blocks.
70+
// For an extended BFS execution, the initial 1975 blocks of every branch are marked in
71+
// the request as being processed, with the aim to receive the last 25 blocks of each fork.
72+
// Performance metrics are captured from the sending of the deserialised request through
73+
// to receiving the response and its deserialisation.
6874
fn e2e(criterion: &mut Criterion) {
6975
let network = Network::Regtest;
7076
let mut processed_block_hashes = vec![];
@@ -173,36 +179,105 @@ fn random_header<const N: usize, R: Rng + CryptoRng>(rng: &mut R) -> [u8; N] {
173179
}
174180

175181
fn add_800k_block_headers(criterion: &mut Criterion) {
176-
static BITCOIN_HEADERS: LazyLock<Vec<bitcoin::block::Header>> = LazyLock::new(|| {
177-
let headers_data_path = PathBuf::from(
178-
std::env::var("BITCOIN_MAINNET_HEADERS_DATA_PATH")
179-
.expect("Failed to get test data path env variable"),
180-
);
181-
retrieve_headers::<bitcoin::Network>(&headers_data_path)
182-
});
183-
// Call BITCOIN_HEADERS once before benchmarking to avoid biasing the first sample (lazy instantiation).
182+
add_block_headers_for(
183+
criterion,
184+
bitcoin::Network::Bitcoin,
185+
"BITCOIN_MAINNET_HEADERS_DATA_PATH",
186+
800_000,
187+
);
188+
add_block_headers_for(
189+
criterion,
190+
bitcoin::dogecoin::Network::Dogecoin,
191+
"DOGECOIN_MAINNET_HEADERS_DATA_PATH",
192+
800_000,
193+
);
194+
}
195+
196+
fn add_block_headers_for<Network: BlockchainNetwork + fmt::Display>(
197+
criterion: &mut Criterion,
198+
network: Network,
199+
headers_data_env: &str,
200+
expected_num_headers_to_add: usize,
201+
) where
202+
Network::Header: for<'de> serde::Deserialize<'de>,
203+
BlockchainState<Network>: HeaderValidator<Network>,
204+
{
205+
let headers_data_path = PathBuf::from(
206+
std::env::var(headers_data_env).expect("Failed to get test data path env variable"),
207+
);
208+
let headers = retrieve_headers::<Network>(&headers_data_path);
184209
// Genesis block header is automatically added when instantiating BlockchainState
185-
let bitcoin_headers_to_add = &BITCOIN_HEADERS.as_slice()[1..];
186-
assert_eq!(bitcoin_headers_to_add.len(), 800_000);
187-
let mut group = criterion.benchmark_group("bitcoin_800k");
210+
let headers_to_add = &headers.as_slice()[1..];
211+
assert_eq!(headers_to_add.len(), expected_num_headers_to_add);
212+
let mut group = criterion.benchmark_group(format!("{network}_{expected_num_headers_to_add}"));
188213
group.sample_size(10);
189214

190-
group.bench_function("add_headers", |bench| {
191-
let rt = tokio::runtime::Runtime::new().unwrap();
215+
bench_add_headers(&mut group, network, headers_to_add);
216+
}
217+
218+
fn bench_add_headers<M: Measurement, Network: BlockchainNetwork>(
219+
group: &mut BenchmarkGroup<'_, M>,
220+
network: Network,
221+
headers: &[Network::Header],
222+
) where
223+
BlockchainState<Network>: HeaderValidator<Network>,
224+
{
225+
fn add_headers<Network: BlockchainNetwork>(
226+
blockchain_state: &mut BlockchainState<Network>,
227+
headers: &[Network::Header],
228+
expect_pruning: bool,
229+
runtime: &tokio::runtime::Runtime,
230+
) where
231+
BlockchainState<Network>: HeaderValidator<Network>,
232+
{
233+
// Genesis block header is automatically added when instantiating BlockchainState
234+
let mut num_added_headers = 1;
235+
// Headers are processed in chunks of at most MAX_HEADERS_SIZE entries
236+
for chunk in headers.chunks(MAX_HEADERS_SIZE) {
237+
let (added_headers, error) =
238+
runtime.block_on(async { blockchain_state.add_headers(chunk).await });
239+
assert!(error.is_none(), "Failed to add headers: {}", error.unwrap());
240+
assert_eq!(added_headers.len(), chunk.len());
241+
num_added_headers += added_headers.len();
242+
243+
runtime
244+
.block_on(async {
245+
blockchain_state
246+
.persist_and_prune_headers_below_anchor(chunk.last().unwrap().block_hash())
247+
.await
248+
})
249+
.unwrap();
250+
let (num_headers_disk, num_headers_memory) = blockchain_state.num_headers().unwrap();
251+
if expect_pruning {
252+
assert_eq!(num_headers_disk, num_added_headers);
253+
assert_eq!(num_headers_memory, 1);
254+
} else {
255+
assert_eq!(num_headers_disk, 0);
256+
assert_eq!(num_headers_memory, num_added_headers);
257+
}
258+
}
259+
}
260+
261+
let rt = tokio::runtime::Runtime::new().unwrap();
262+
263+
group.bench_function(BenchmarkId::new("add_headers", "in_memory"), |bench| {
264+
bench.iter(|| {
265+
let mut blockchain_state =
266+
BlockchainState::new(network, None, &MetricsRegistry::default(), no_op_logger());
267+
add_headers(&mut blockchain_state, headers, false, &rt);
268+
})
269+
});
270+
271+
group.bench_function(BenchmarkId::new("add_headers", "lmdb"), |bench| {
192272
bench.iter(|| {
193-
let blockchain_state = BlockchainState::new(
194-
Network::Bitcoin,
195-
None,
273+
let dir = tempdir().unwrap();
274+
let mut blockchain_state = BlockchainState::new(
275+
network,
276+
Some(dir.path().to_path_buf()),
196277
&MetricsRegistry::default(),
197278
no_op_logger(),
198279
);
199-
// Headers are processed in chunks of at most MAX_HEADERS_SIZE entries
200-
for chunk in bitcoin_headers_to_add.chunks(MAX_HEADERS_SIZE) {
201-
let (added_headers, error) =
202-
rt.block_on(async { blockchain_state.add_headers(chunk).await });
203-
assert!(error.is_none(), "Failed to add headers: {}", error.unwrap());
204-
assert_eq!(added_headers.len(), chunk.len())
205-
}
280+
add_headers(&mut blockchain_state, headers, true, &rt);
206281
})
207282
});
208283
}
@@ -232,11 +307,6 @@ fn decompress<P: AsRef<Path>>(location: P) -> Vec<u8> {
232307
decompressed
233308
}
234309

235-
// This simulation constructs a blockchain comprising four forks, each of 2000 blocks.
236-
// For an extended BFS execution, the initial 1975 blocks of every branch are marked in
237-
// the request as being processed, with the aim to receive the last 25 blocks of each fork.
238-
// Performance metrics are captured from the sending of the deserialised request through
239-
// to receiving the response and its deserialisation.
240310
criterion_group!(benches, e2e, hash_block_header, add_800k_block_headers);
241311

242312
// The benchmark can be run using:

rs/bitcoin/adapter/src/blockchainstate.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,19 @@ where
203203
Ok(result)
204204
}
205205

206+
/// Background task to ersist headers below the anchor (as headers) and the anchor (as tip) on to disk, and
207+
/// prune headers below the anchor from the in-memory cache.
208+
pub fn persist_and_prune_headers_below_anchor(
209+
&self,
210+
anchor: BlockHash,
211+
) -> tokio::task::JoinHandle<()> {
212+
let header_cache = self.header_cache.clone();
213+
tokio::task::spawn_blocking(move || {
214+
// Error is ignored, since it is a background task
215+
let _ = header_cache.persist_and_prune_headers_below_anchor(anchor);
216+
})
217+
}
218+
206219
/// This method adds a new block to the `block_cache`
207220
pub async fn add_block(
208221
&self,
@@ -333,6 +346,18 @@ where
333346
.map(|block| block.len())
334347
.sum()
335348
}
349+
350+
/// Number of headers stored.
351+
///
352+
/// Return a pair where
353+
/// 1. Number of headers stored on disk
354+
/// 2. Number of headers stored in memory
355+
pub fn num_headers(&self) -> Result<(usize, usize), String> {
356+
self.header_cache
357+
.get_num_headers()
358+
// do not expose internal error type
359+
.map_err(|e| e.to_string())
360+
}
336361
}
337362

338363
impl<Network: BlockchainNetwork> HeaderStore for BlockchainState<Network> {

rs/bitcoin/adapter/src/get_successors_handler.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,16 @@ impl<Network: BlockchainNetwork + Send + Sync> GetSuccessorsHandler<Network> {
127127

128128
// Spawn persist-to-disk task without waiting for it to finish, and make sure there
129129
// is only one task running at a time.
130-
let cache = self.state.header_cache.clone();
131130
let mut handle = self.pruning_task_handle.lock().unwrap();
132131
let is_finished = handle
133132
.as_ref()
134133
.map(|handle| handle.is_finished())
135134
.unwrap_or(true);
136135
if is_finished {
137-
*handle = Some(tokio::task::spawn_blocking(move || {
138-
// Error is ignored, since it is a background task
139-
let _ = cache.persist_and_prune_headers_below_anchor(request.anchor);
140-
}));
136+
*handle = Some(
137+
self.state
138+
.persist_and_prune_headers_below_anchor(request.anchor),
139+
);
141140
}
142141

143142
let (blocks, next, obsolete_blocks) = {

rs/bitcoin/adapter/src/header_cache.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ pub trait HeaderCache: Send + Sync {
146146
/// Return the number of tips.
147147
fn get_num_tips(&self) -> usize;
148148

149+
/// Return the number of headers.
150+
fn get_num_headers(&self) -> usize;
151+
149152
/// Return the ancestor from the given block hash to the current anchor in the
150153
/// in-memory cache as a chain of headers, where each element is the only child
151154
/// of the next, and the first element (tip) has no child.
@@ -227,6 +230,10 @@ impl<Header: BlockchainHeader + Send + Sync> HeaderCache for RwLock<InMemoryHead
227230
self.read().unwrap().tips.len()
228231
}
229232

233+
fn get_num_headers(&self) -> usize {
234+
self.read().unwrap().cache.len()
235+
}
236+
230237
fn get_ancestor_chain(&self, from: BlockHash) -> Vec<(BlockHash, HeaderNode<Header>)> {
231238
let mut hash = from;
232239
let mut to_persist = Vec::new();
@@ -362,6 +369,18 @@ impl LMDBHeaderCache {
362369
Ok(node)
363370
}
364371

372+
fn tx_get_num_headers<Tx: Transaction>(&self, tx: &Tx) -> Result<usize, LMDBCacheError> {
373+
let num = tx
374+
.stat(self.headers)
375+
.map(|stat| stat.entries())
376+
.map_err(LMDBCacheError::Lmdb)?;
377+
assert!(
378+
num > 0,
379+
"BUG: LMDBHeaderCache::new_with_genesis adds the tip header key '{TIP_KEY}'"
380+
);
381+
Ok(num - 1)
382+
}
383+
365384
fn tx_add_header<Header: BlockchainHeader>(
366385
&self,
367386
tx: &mut RwTransaction,
@@ -528,6 +547,27 @@ impl<Header: BlockchainHeader + Send + Sync + 'static> HybridHeaderCache<Header>
528547
})
529548
}
530549

550+
/// Number of headers stored.
551+
///
552+
/// Return a pair where
553+
/// 1. Number of headers stored on disk
554+
/// 2. Number of headers stored in memory
555+
pub fn get_num_headers(&self) -> Result<(usize, usize), LMDBCacheError> {
556+
let num_headers_in_memory = self.in_memory.get_num_headers();
557+
if self.on_disk.is_none() {
558+
return Ok((0, num_headers_in_memory));
559+
}
560+
561+
let cache = self.on_disk.as_ref().unwrap();
562+
let num_headers_on_disk = log_err!(
563+
cache.run_ro_txn(|tx| cache.tx_get_num_headers(tx)),
564+
cache.log,
565+
"get_num_headers"
566+
)?;
567+
568+
Ok((num_headers_on_disk, num_headers_in_memory))
569+
}
570+
531571
/// Get a header by hash.
532572
pub fn get_header(&self, hash: BlockHash) -> Option<HeaderNode<Header>> {
533573
self.in_memory

rs/bitcoin/adapter/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ enum ProcessNetworkMessageError {
8080
InvalidMessage,
8181
}
8282

83-
/// This enum is used to represent errors that
83+
/// Error returned by `Channel::send`.
8484
#[derive(Debug)]
8585
enum ChannelError {}
8686

rs/bitcoin/validation/src/header.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
};
1414

1515
/// An error thrown when trying to validate a header.
16-
#[derive(Debug, PartialEq)]
16+
#[derive(Debug, Eq, PartialEq)]
1717
pub enum ValidateHeaderError {
1818
/// Used when the timestamp in the header is lower than
1919
/// the median of timestamps of past 11 headers.

0 commit comments

Comments
 (0)