Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions crates/esplora/src/async_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,12 @@ async fn chain_update<S: Sleeper>(

// insert the most recent blocks at the tip to make sure we update the tip and make the update
// robust.
for (&height, &hash) in latest_blocks.iter() {
//
// Height 0 is skipped: genesis stays immutable in a `CheckPoint`, so an entry agreeing with our
// genesis is a no-op while one disagreeing with it panics `insert`, poisoning every later sync.
// A server on a different chain is still rejected above, where we fail to find a point of
// agreement.
for (&height, &hash) in latest_blocks.range(1..) {
tip = tip.insert(height, hash);
}

Expand Down Expand Up @@ -556,7 +561,10 @@ where
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
use std::{collections::BTreeSet, time::Duration};
use std::{
collections::{BTreeMap, BTreeSet},
time::Duration,
};

use bdk_chain::{
bitcoin::{hashes::Hash, Txid},
Expand All @@ -565,7 +573,7 @@ mod test {
};
use bdk_core::{bitcoin, ConfirmationBlockTime};
use bdk_testenv::{anyhow, TestEnv};
use esplora_client::Builder;
use esplora_client::{BlockHash, Builder};

use crate::async_ext::{chain_update, fetch_latest_blocks};

Expand Down Expand Up @@ -610,6 +618,34 @@ mod test {
Ok(())
}

// Test that `chain_update` does not panic when the `/blocks` response carries a height-0
// entry conflicting with the local genesis.
// See <https://github.com/bitcoindevkit/bdk/issues/2341>.
#[tokio::test]
async fn test_chain_update_ignores_genesis_in_latest_blocks() -> anyhow::Result<()> {
// `latest_blocks` covers the local tip height, so no request is made.
let client = Builder::new("http://127.0.0.1:1").build_async()?;

let genesis_hash: BlockHash = h!("genesis");
let block_1_hash: BlockHash = h!("block 1");
let local_tip = bdk_chain::CheckPoint::new(0, genesis_hash)
.push(1, block_1_hash)
.expect("height is above genesis");

let latest_blocks = BTreeMap::from([(0, h!("other genesis")), (1, block_1_hash)]);

let update = chain_update(&client, &latest_blocks, &local_tip, &BTreeSet::new()).await?;

assert_eq!(update.height(), 1);
assert_eq!(
update.get(0).map(|cp| cp.hash()),
Some(genesis_hash),
"the local genesis must be left untouched",
);

Ok(())
}

/// Ensure that update does not remove heights (from original), and all anchor heights are
/// included.
#[tokio::test]
Expand Down
35 changes: 34 additions & 1 deletion crates/esplora/src/blocking_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,12 @@ fn chain_update(

// insert the most recent blocks at the tip to make sure we update the tip and make the update
// robust.
for (&height, &hash) in latest_blocks.iter() {
//
// Height 0 is skipped: genesis stays immutable in a `CheckPoint`, so an entry agreeing with our
// genesis is a no-op while one disagreeing with it panics `insert`, poisoning every later sync.
// A server on a different chain is still rejected above, where we fail to find a point of
// agreement.
for (&height, &hash) in latest_blocks.range(1..) {
tip = tip.insert(height, hash);
}

Expand Down Expand Up @@ -573,6 +578,34 @@ mod test {
Ok(())
}

// Test that `chain_update` does not panic when the `/blocks` response carries a height-0
// entry conflicting with the local genesis.
// See <https://github.com/bitcoindevkit/bdk/issues/2341>.
#[test]
fn test_chain_update_ignores_genesis_in_latest_blocks() -> anyhow::Result<()> {
// `latest_blocks` covers the local tip height, so no request is made.
let client = Builder::new("http://127.0.0.1:1").build_blocking();

let genesis_hash: BlockHash = h!("genesis");
let block_1_hash: BlockHash = h!("block 1");
let local_tip = bdk_chain::CheckPoint::new(0, genesis_hash)
.push(1, block_1_hash)
.expect("height is above genesis");

let latest_blocks = BTreeMap::from([(0, h!("other genesis")), (1, block_1_hash)]);

let update = chain_update(&client, &latest_blocks, &local_tip, &BTreeSet::new())?;

assert_eq!(update.height(), 1);
assert_eq!(
update.get(0).map(|cp| cp.hash()),
Some(genesis_hash),
"the local genesis must be left untouched",
);

Ok(())
}

/// Ensure that update does not remove heights (from original), and all anchor heights are
/// included.
#[test]
Expand Down
Loading