Skip to content

Commit bd62aa0

Browse files
committed
feat(esplora)!: remove EsploraExt::update_local_chain
Previously, we would update the `TxGraph` and `KeychainTxOutIndex` first, then create a second update for `LocalChain`. This required locking the receiving structures 3 times (instead of twice, which is optimal). This PR eliminates this requirement by making use of the new `query` method of `CheckPoint`. Examples are also updated to use the new API.
1 parent 1e99793 commit bd62aa0

File tree

9 files changed

+861
-556
lines changed

9 files changed

+861
-556
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 307 additions & 225 deletions
Large diffs are not rendered by default.

crates/esplora/src/blocking_ext.rs

Lines changed: 327 additions & 225 deletions
Large diffs are not rendered by default.

crates/esplora/src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
1717
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
1818
19-
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
19+
use std::collections::BTreeMap;
20+
21+
use bdk_chain::{local_chain, BlockId, ConfirmationTimeHeightAnchor, TxGraph};
2022
use esplora_client::TxStatus;
2123

2224
pub use esplora_client;
@@ -48,3 +50,21 @@ fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor>
4850
None
4951
}
5052
}
53+
54+
/// Update returns from a full scan.
55+
pub struct FullScanUpdate<K> {
56+
/// The update to apply to the receiving [`LocalChain`](local_chain::LocalChain).
57+
pub local_chain: local_chain::Update,
58+
/// The update to apply to the receiving [`TxGraph`].
59+
pub tx_graph: TxGraph<ConfirmationTimeHeightAnchor>,
60+
/// Last active indices for the corresponding keychains (`K`).
61+
pub last_active_indices: BTreeMap<K, u32>,
62+
}
63+
64+
/// Update returned from a sync.
65+
pub struct SyncUpdate {
66+
/// The update to apply to the receiving [`LocalChain`](local_chain::LocalChain).
67+
pub local_chain: local_chain::Update,
68+
/// The update to apply to the receiving [`TxGraph`].
69+
pub tx_graph: TxGraph<ConfirmationTimeHeightAnchor>,
70+
}

crates/esplora/tests/async_ext.rs

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bdk_esplora::EsploraAsyncExt;
22
use electrsd::bitcoind::anyhow;
33
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
44
use esplora_client::{self, Builder};
5-
use std::collections::{BTreeMap, HashSet};
5+
use std::collections::{BTreeMap, BTreeSet, HashSet};
66
use std::str::FromStr;
77
use std::thread::sleep;
88
use std::time::Duration;
@@ -52,15 +52,37 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
5252
sleep(Duration::from_millis(10))
5353
}
5454

55-
let graph_update = client
55+
// use a full checkpoint linked list (since this is not what we are testing)
56+
let cp_tip = env.make_checkpoint_tip();
57+
58+
let sync_update = client
5659
.sync(
60+
cp_tip.clone(),
5761
misc_spks.into_iter(),
5862
vec![].into_iter(),
5963
vec![].into_iter(),
6064
1,
6165
)
6266
.await?;
6367

68+
assert!(
69+
{
70+
let update_cps = sync_update
71+
.local_chain
72+
.tip
73+
.iter()
74+
.map(|cp| cp.block_id())
75+
.collect::<BTreeSet<_>>();
76+
let superset_cps = cp_tip
77+
.iter()
78+
.map(|cp| cp.block_id())
79+
.collect::<BTreeSet<_>>();
80+
superset_cps.is_superset(&update_cps)
81+
},
82+
"update should not alter original checkpoint tip since we already started with all checkpoints",
83+
);
84+
85+
let graph_update = sync_update.tx_graph;
6486
// Check to see if we have the floating txouts available from our two created transactions'
6587
// previous outputs in order to calculate transaction fees.
6688
for tx in graph_update.full_txs() {
@@ -140,14 +162,24 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
140162
sleep(Duration::from_millis(10))
141163
}
142164

165+
// use a full checkpoint linked list (since this is not what we are testing)
166+
let cp_tip = env.make_checkpoint_tip();
167+
143168
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
144169
// will.
145-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
146-
assert!(graph_update.full_txs().next().is_none());
147-
assert!(active_indices.is_empty());
148-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1).await?;
149-
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
150-
assert_eq!(active_indices[&0], 3);
170+
let full_scan_update = client
171+
.full_scan(cp_tip.clone(), keychains.clone(), 3, 1)
172+
.await?;
173+
assert!(full_scan_update.tx_graph.full_txs().next().is_none());
174+
assert!(full_scan_update.last_active_indices.is_empty());
175+
let full_scan_update = client
176+
.full_scan(cp_tip.clone(), keychains.clone(), 4, 1)
177+
.await?;
178+
assert_eq!(
179+
full_scan_update.tx_graph.full_txs().next().unwrap().txid,
180+
txid_4th_addr
181+
);
182+
assert_eq!(full_scan_update.last_active_indices[&0], 3);
151183

152184
// Now receive a coin on the last address.
153185
let txid_last_addr = env.bitcoind.client.send_to_address(
@@ -167,16 +199,26 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
167199

168200
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
169201
// The last active indice won't be updated in the first case but will in the second one.
170-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1).await?;
171-
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
202+
let full_scan_update = client
203+
.full_scan(cp_tip.clone(), keychains.clone(), 5, 1)
204+
.await?;
205+
let txs: HashSet<_> = full_scan_update
206+
.tx_graph
207+
.full_txs()
208+
.map(|tx| tx.txid)
209+
.collect();
172210
assert_eq!(txs.len(), 1);
173211
assert!(txs.contains(&txid_4th_addr));
174-
assert_eq!(active_indices[&0], 3);
175-
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1).await?;
176-
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
212+
assert_eq!(full_scan_update.last_active_indices[&0], 3);
213+
let full_scan_update = client.full_scan(cp_tip, keychains, 6, 1).await?;
214+
let txs: HashSet<_> = full_scan_update
215+
.tx_graph
216+
.full_txs()
217+
.map(|tx| tx.txid)
218+
.collect();
177219
assert_eq!(txs.len(), 2);
178220
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
179-
assert_eq!(active_indices[&0], 9);
221+
assert_eq!(full_scan_update.last_active_indices[&0], 9);
180222

181223
Ok(())
182224
}

crates/esplora/tests/blocking_ext.rs

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use bdk_chain::BlockId;
33
use bdk_esplora::EsploraExt;
44
use electrsd::bitcoind::anyhow;
55
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
6-
use esplora_client::{self, Builder};
6+
use esplora_client::{self, BlockHash, Builder};
77
use std::collections::{BTreeMap, BTreeSet, HashSet};
88
use std::str::FromStr;
99
use std::thread::sleep;
@@ -68,13 +68,35 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
6868
sleep(Duration::from_millis(10))
6969
}
7070

71-
let graph_update = client.sync(
71+
// use a full checkpoint linked list (since this is not what we are testing)
72+
let cp_tip = env.make_checkpoint_tip();
73+
74+
let sync_update = client.sync(
75+
cp_tip.clone(),
7276
misc_spks.into_iter(),
7377
vec![].into_iter(),
7478
vec![].into_iter(),
7579
1,
7680
)?;
7781

82+
assert!(
83+
{
84+
let update_cps = sync_update
85+
.local_chain
86+
.tip
87+
.iter()
88+
.map(|cp| cp.block_id())
89+
.collect::<BTreeSet<_>>();
90+
let superset_cps = cp_tip
91+
.iter()
92+
.map(|cp| cp.block_id())
93+
.collect::<BTreeSet<_>>();
94+
superset_cps.is_superset(&update_cps)
95+
},
96+
"update should not alter original checkpoint tip since we already started with all checkpoints",
97+
);
98+
99+
let graph_update = sync_update.tx_graph;
78100
// Check to see if we have the floating txouts available from our two created transactions'
79101
// previous outputs in order to calculate transaction fees.
80102
for tx in graph_update.full_txs() {
@@ -155,14 +177,20 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
155177
sleep(Duration::from_millis(10))
156178
}
157179

180+
// use a full checkpoint linked list (since this is not what we are testing)
181+
let cp_tip = env.make_checkpoint_tip();
182+
158183
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
159184
// will.
160-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
161-
assert!(graph_update.full_txs().next().is_none());
162-
assert!(active_indices.is_empty());
163-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 1)?;
164-
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
165-
assert_eq!(active_indices[&0], 3);
185+
let full_scan_update = client.full_scan(cp_tip.clone(), keychains.clone(), 3, 1)?;
186+
assert!(full_scan_update.tx_graph.full_txs().next().is_none());
187+
assert!(full_scan_update.last_active_indices.is_empty());
188+
let full_scan_update = client.full_scan(cp_tip.clone(), keychains.clone(), 4, 1)?;
189+
assert_eq!(
190+
full_scan_update.tx_graph.full_txs().next().unwrap().txid,
191+
txid_4th_addr
192+
);
193+
assert_eq!(full_scan_update.last_active_indices[&0], 3);
166194

167195
// Now receive a coin on the last address.
168196
let txid_last_addr = env.bitcoind.client.send_to_address(
@@ -182,16 +210,24 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
182210

183211
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
184212
// The last active indice won't be updated in the first case but will in the second one.
185-
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 1)?;
186-
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
213+
let full_scan_update = client.full_scan(cp_tip.clone(), keychains.clone(), 5, 1)?;
214+
let txs: HashSet<_> = full_scan_update
215+
.tx_graph
216+
.full_txs()
217+
.map(|tx| tx.txid)
218+
.collect();
187219
assert_eq!(txs.len(), 1);
188220
assert!(txs.contains(&txid_4th_addr));
189-
assert_eq!(active_indices[&0], 3);
190-
let (graph_update, active_indices) = client.full_scan(keychains, 6, 1)?;
191-
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
221+
assert_eq!(full_scan_update.last_active_indices[&0], 3);
222+
let full_scan_update = client.full_scan(cp_tip.clone(), keychains, 6, 1)?;
223+
let txs: HashSet<_> = full_scan_update
224+
.tx_graph
225+
.full_txs()
226+
.map(|tx| tx.txid)
227+
.collect();
192228
assert_eq!(txs.len(), 2);
193229
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
194-
assert_eq!(active_indices[&0], 9);
230+
assert_eq!(full_scan_update.last_active_indices[&0], 9);
195231

196232
Ok(())
197233
}
@@ -317,14 +353,38 @@ fn update_local_chain() -> anyhow::Result<()> {
317353
for (i, t) in test_cases.into_iter().enumerate() {
318354
println!("Case {}: {}", i, t.name);
319355
let mut chain = t.chain;
356+
let cp_tip = chain.tip();
320357

321-
let update = client
322-
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
323-
.map_err(|err| {
324-
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
358+
let new_blocks =
359+
bdk_esplora::init_chain_update_blocking(&client, &cp_tip).map_err(|err| {
360+
anyhow::format_err!("[{}:{}] `init_chain_update` failed: {}", i, t.name, err)
325361
})?;
326362

327-
let update_blocks = update
363+
let mock_anchors = t
364+
.request_heights
365+
.iter()
366+
.map(|&h| {
367+
let anchor_blockhash: BlockHash = bdk_chain::bitcoin::hashes::Hash::hash(
368+
&format!("hash_at_height_{}", h).into_bytes(),
369+
);
370+
let txid: Txid = bdk_chain::bitcoin::hashes::Hash::hash(
371+
&format!("txid_at_height_{}", h).into_bytes(),
372+
);
373+
let anchor = BlockId {
374+
height: h,
375+
hash: anchor_blockhash,
376+
};
377+
(anchor, txid)
378+
})
379+
.collect::<BTreeSet<_>>();
380+
381+
let chain_update = bdk_esplora::finalize_chain_update_blocking(
382+
&client,
383+
&cp_tip,
384+
&mock_anchors,
385+
new_blocks,
386+
)?;
387+
let update_blocks = chain_update
328388
.tip
329389
.iter()
330390
.map(|cp| cp.block_id())
@@ -346,14 +406,15 @@ fn update_local_chain() -> anyhow::Result<()> {
346406
)
347407
.collect::<BTreeSet<_>>();
348408

349-
assert_eq!(
350-
update_blocks, exp_update_blocks,
409+
assert!(
410+
update_blocks.is_superset(&exp_update_blocks),
351411
"[{}:{}] unexpected update",
352-
i, t.name
412+
i,
413+
t.name
353414
);
354415

355416
let _ = chain
356-
.apply_update(update)
417+
.apply_update(chain_update)
357418
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
358419

359420
// all requested heights must exist in the final chain

crates/testenv/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use bdk_chain::{
22
bitcoin::{
3-
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash, secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid
3+
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
4+
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
5+
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
46
},
57
local_chain::CheckPoint,
68
BlockId,

0 commit comments

Comments
 (0)