Skip to content

Commit dd394cb

Browse files
committed
fix(esplora): chain_update errors if no point of connection
Before, the `chain_update` function would hit a panic if the local checkpoint was not on the same network as the remote server. Now if we have iterated all of the blocks of the `local_cp` and do not find a "point of agreement", then we return early with a `esplora_client::Error::HeaderHashNotFound`.
1 parent 63923c6 commit dd394cb

File tree

2 files changed

+118
-33
lines changed

2 files changed

+118
-33
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,10 @@ async fn fetch_block<S: Sleeper>(
205205

206206
// We avoid fetching blocks higher than previously fetched `latest_blocks` as the local chain
207207
// tip is used to signal for the last-synced-up-to-height.
208-
let &tip_height = latest_blocks
209-
.keys()
210-
.last()
211-
.expect("must have atleast one entry");
212-
if height > tip_height {
213-
return Ok(None);
208+
if let Some(tip_height) = latest_blocks.keys().last().copied() {
209+
if height > tip_height {
210+
return Ok(None);
211+
}
214212
}
215213

216214
Ok(Some(client.get_block_hash(height).await?))
@@ -227,27 +225,36 @@ async fn chain_update<S: Sleeper>(
227225
anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>,
228226
) -> Result<CheckPoint, Error> {
229227
let mut point_of_agreement = None;
228+
let mut local_cp_hash = local_tip.hash();
230229
let mut conflicts = vec![];
230+
231231
for local_cp in local_tip.iter() {
232232
let remote_hash = match fetch_block(client, latest_blocks, local_cp.height()).await? {
233233
Some(hash) => hash,
234234
None => continue,
235235
};
236236
if remote_hash == local_cp.hash() {
237-
point_of_agreement = Some(local_cp.clone());
237+
point_of_agreement = Some(local_cp);
238238
break;
239-
} else {
240-
// it is not strictly necessary to include all the conflicted heights (we do need the
241-
// first one) but it seems prudent to make sure the updated chain's heights are a
242-
// superset of the existing chain after update.
243-
conflicts.push(BlockId {
244-
height: local_cp.height(),
245-
hash: remote_hash,
246-
});
247239
}
240+
local_cp_hash = local_cp.hash();
241+
// It is not strictly necessary to include all the conflicted heights (we do need the
242+
// first one) but it seems prudent to make sure the updated chain's heights are a
243+
// superset of the existing chain after update.
244+
conflicts.push(BlockId {
245+
height: local_cp.height(),
246+
hash: remote_hash,
247+
});
248248
}
249249

250-
let mut tip = point_of_agreement.expect("remote esplora should have same genesis block");
250+
let mut tip = match point_of_agreement {
251+
Some(tip) => tip,
252+
None => {
253+
return Err(Box::new(esplora_client::Error::HeaderHashNotFound(
254+
local_cp_hash,
255+
)));
256+
}
257+
};
251258

252259
tip = tip
253260
.extend(conflicts.into_iter().rev())
@@ -545,7 +552,7 @@ mod test {
545552
local_chain::LocalChain,
546553
BlockId,
547554
};
548-
use bdk_core::ConfirmationBlockTime;
555+
use bdk_core::{bitcoin, ConfirmationBlockTime};
549556
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
550557
use esplora_client::Builder;
551558

@@ -557,6 +564,41 @@ mod test {
557564
}};
558565
}
559566

567+
// Test that `chain_update` fails due to wrong network.
568+
#[tokio::test]
569+
async fn test_chain_update_wrong_network_error() -> anyhow::Result<()> {
570+
let env = TestEnv::new()?;
571+
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
572+
let client = Builder::new(base_url.as_str()).build_async()?;
573+
let initial_height = env.rpc_client().get_block_count()? as u32;
574+
575+
let mine_to = 16;
576+
let _ = env.mine_blocks((mine_to - initial_height) as usize, None)?;
577+
while client.get_height().await? < mine_to {
578+
std::thread::sleep(Duration::from_millis(64));
579+
}
580+
let latest_blocks = fetch_latest_blocks(&client).await?;
581+
assert!(!latest_blocks.is_empty());
582+
assert_eq!(latest_blocks.keys().last(), Some(&mine_to));
583+
584+
let genesis_hash =
585+
bitcoin::constants::genesis_block(bitcoin::Network::Testnet4).block_hash();
586+
let cp = bdk_chain::CheckPoint::new(BlockId {
587+
height: 0,
588+
hash: genesis_hash,
589+
});
590+
591+
let anchors = BTreeSet::new();
592+
let res = chain_update(&client, &latest_blocks, &cp, &anchors).await;
593+
use esplora_client::Error;
594+
assert!(
595+
matches!(*res.unwrap_err(), Error::HeaderHashNotFound(hash) if hash == genesis_hash),
596+
"`chain_update` should error if it can't connect to the local CP",
597+
);
598+
599+
Ok(())
600+
}
601+
560602
/// Ensure that update does not remove heights (from original), and all anchor heights are
561603
/// included.
562604
#[tokio::test]

crates/esplora/src/blocking_ext.rs

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,10 @@ fn fetch_block(
190190

191191
// We avoid fetching blocks higher than previously fetched `latest_blocks` as the local chain
192192
// tip is used to signal for the last-synced-up-to-height.
193-
let &tip_height = latest_blocks
194-
.keys()
195-
.last()
196-
.expect("must have atleast one entry");
197-
if height > tip_height {
198-
return Ok(None);
193+
if let Some(tip_height) = latest_blocks.keys().last().copied() {
194+
if height > tip_height {
195+
return Ok(None);
196+
}
199197
}
200198

201199
Ok(Some(client.get_block_hash(height)?))
@@ -212,27 +210,36 @@ fn chain_update(
212210
anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>,
213211
) -> Result<CheckPoint, Error> {
214212
let mut point_of_agreement = None;
213+
let mut local_cp_hash = local_tip.hash();
215214
let mut conflicts = vec![];
215+
216216
for local_cp in local_tip.iter() {
217217
let remote_hash = match fetch_block(client, latest_blocks, local_cp.height())? {
218218
Some(hash) => hash,
219219
None => continue,
220220
};
221221
if remote_hash == local_cp.hash() {
222-
point_of_agreement = Some(local_cp.clone());
222+
point_of_agreement = Some(local_cp);
223223
break;
224-
} else {
225-
// it is not strictly necessary to include all the conflicted heights (we do need the
226-
// first one) but it seems prudent to make sure the updated chain's heights are a
227-
// superset of the existing chain after update.
228-
conflicts.push(BlockId {
229-
height: local_cp.height(),
230-
hash: remote_hash,
231-
});
232224
}
225+
local_cp_hash = local_cp.hash();
226+
// It is not strictly necessary to include all the conflicted heights (we do need the
227+
// first one) but it seems prudent to make sure the updated chain's heights are a
228+
// superset of the existing chain after update.
229+
conflicts.push(BlockId {
230+
height: local_cp.height(),
231+
hash: remote_hash,
232+
});
233233
}
234234

235-
let mut tip = point_of_agreement.expect("remote esplora should have same genesis block");
235+
let mut tip = match point_of_agreement {
236+
Some(tip) => tip,
237+
None => {
238+
return Err(Box::new(esplora_client::Error::HeaderHashNotFound(
239+
local_cp_hash,
240+
)));
241+
}
242+
};
236243

237244
tip = tip
238245
.extend(conflicts.into_iter().rev())
@@ -498,6 +505,7 @@ fn fetch_txs_with_outpoints<I: IntoIterator<Item = OutPoint>>(
498505
#[cfg(test)]
499506
mod test {
500507
use crate::blocking_ext::{chain_update, fetch_latest_blocks};
508+
use bdk_chain::bitcoin;
501509
use bdk_chain::bitcoin::hashes::Hash;
502510
use bdk_chain::bitcoin::Txid;
503511
use bdk_chain::local_chain::LocalChain;
@@ -522,6 +530,41 @@ mod test {
522530
}};
523531
}
524532

533+
// Test that `chain_update` fails due to wrong network.
534+
#[test]
535+
fn test_chain_update_wrong_network_error() -> anyhow::Result<()> {
536+
let env = TestEnv::new()?;
537+
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
538+
let client = Builder::new(base_url.as_str()).build_blocking();
539+
let initial_height = env.rpc_client().get_block_count()? as u32;
540+
541+
let mine_to = 16;
542+
let _ = env.mine_blocks((mine_to - initial_height) as usize, None)?;
543+
while client.get_height()? < mine_to {
544+
std::thread::sleep(Duration::from_millis(64));
545+
}
546+
let latest_blocks = fetch_latest_blocks(&client)?;
547+
assert!(!latest_blocks.is_empty());
548+
assert_eq!(latest_blocks.keys().last(), Some(&mine_to));
549+
550+
let genesis_hash =
551+
bitcoin::constants::genesis_block(bitcoin::Network::Testnet4).block_hash();
552+
let cp = bdk_chain::CheckPoint::new(BlockId {
553+
height: 0,
554+
hash: genesis_hash,
555+
});
556+
557+
let anchors = BTreeSet::new();
558+
let res = chain_update(&client, &latest_blocks, &cp, &anchors);
559+
use esplora_client::Error;
560+
assert!(
561+
matches!(*res.unwrap_err(), Error::HeaderHashNotFound(hash) if hash == genesis_hash),
562+
"`chain_update` should error if it can't connect to the local CP",
563+
);
564+
565+
Ok(())
566+
}
567+
525568
/// Ensure that update does not remove heights (from original), and all anchor heights are
526569
/// included.
527570
#[test]

0 commit comments

Comments
 (0)