feat(electrum)!: use new sync/full-scan structs for ElectrumExt

`ElectrumResultExt` trait is also introduced that adds methods which can
convert the `Anchor` type for the update `TxGraph`.

We also make use of the new `TxCache` fields in
`SyncRequest`/`FullScanRequest`. This way, we can avoid re-fetching full
transactions from Electrum if not needed.

Examples and tests are updated to use the new `ElectrumExt` API.
This commit is contained in:
志宇 2024-04-30 14:50:21 +08:00
parent 653e4fed6d
commit a6fdfb2ae4
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
5 changed files with 284 additions and 325 deletions

View File

@ -12,7 +12,7 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
bdk_chain = { path = "../chain", version = "0.13.0", default-features = false } bdk_chain = { path = "../chain", version = "0.13.0" }
electrum-client = { version = "0.19" } electrum-client = { version = "0.19" }
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] } #rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }

View File

@ -1,28 +1,18 @@
use bdk_chain::{ use bdk_chain::{
bitcoin::{OutPoint, ScriptBuf, Txid}, bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
collections::{HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
local_chain::CheckPoint, local_chain::CheckPoint,
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult, TxCache},
tx_graph::TxGraph, tx_graph::TxGraph,
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
}; };
use core::str::FromStr;
use electrum_client::{ElectrumApi, Error, HeaderNotification}; use electrum_client::{ElectrumApi, Error, HeaderNotification};
use std::{collections::BTreeMap, fmt::Debug, str::FromStr}; use std::sync::Arc;
/// We include a chain suffix of a certain length for the purpose of robustness. /// We include a chain suffix of a certain length for the purpose of robustness.
const CHAIN_SUFFIX_LENGTH: u32 = 8; const CHAIN_SUFFIX_LENGTH: u32 = 8;
/// Combination of chain and transactions updates from electrum
///
/// We have to update the chain and the txids at the same time since we anchor the txids to
/// the same chain tip that we check before and after we gather the txids.
#[derive(Debug)]
pub struct ElectrumUpdate {
/// Chain update
pub chain_update: CheckPoint,
/// Tracks electrum updates in TxGraph
pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
}
/// Trait to extend [`electrum_client::Client`] functionality. /// Trait to extend [`electrum_client::Client`] functionality.
pub trait ElectrumExt { pub trait ElectrumExt {
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and /// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
@ -35,14 +25,12 @@ pub trait ElectrumExt {
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a /// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
/// single batch request. /// single batch request.
fn full_scan<K: Ord + Clone, A: Anchor>( fn full_scan<K: Ord + Clone>(
&self, &self,
prev_tip: CheckPoint, request: FullScanRequest<K>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
full_txs: Option<&TxGraph<A>>,
stop_gap: usize, stop_gap: usize,
batch_size: usize, batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>; ) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error>;
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
/// and returns updates for [`bdk_chain`] data structures. /// and returns updates for [`bdk_chain`] data structures.
@ -61,42 +49,33 @@ pub trait ElectrumExt {
/// may include scripts that have been used, use [`full_scan`] with the keychain. /// may include scripts that have been used, use [`full_scan`] with the keychain.
/// ///
/// [`full_scan`]: ElectrumExt::full_scan /// [`full_scan`]: ElectrumExt::full_scan
fn sync<A: Anchor>( fn sync(
&self, &self,
prev_tip: CheckPoint, request: SyncRequest,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
full_txs: Option<&TxGraph<A>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
batch_size: usize, batch_size: usize,
) -> Result<ElectrumUpdate, Error>; ) -> Result<SyncResult<ConfirmationHeightAnchor>, Error>;
} }
impl<E: ElectrumApi> ElectrumExt for E { impl<E: ElectrumApi> ElectrumExt for E {
fn full_scan<K: Ord + Clone, A: Anchor>( fn full_scan<K: Ord + Clone>(
&self, &self,
prev_tip: CheckPoint, mut request: FullScanRequest<K>,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
full_txs: Option<&TxGraph<A>>,
stop_gap: usize, stop_gap: usize,
batch_size: usize, batch_size: usize,
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> { ) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error> {
let mut request_spks = keychain_spks let mut request_spks = request.spks_by_keychain;
.into_iter()
.map(|(k, s)| (k, s.into_iter())) // We keep track of already-scanned spks just in case a reorg happens and we need to do a
.collect::<BTreeMap<K, _>>(); // rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
// cannot be collected. In addition, we keep track of whether an spk has an active tx
// history for determining the `last_active_index`.
// * key: (keychain, spk_index) that identifies the spk.
// * val: (script_pubkey, has_tx_history).
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new(); let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
let (electrum_update, keychain_update) = loop { let update = loop {
let (tip, _) = construct_update_tip(self, prev_tip.clone())?; let (tip, _) = construct_update_tip(self, request.chain_tip.clone())?;
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default(); let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
if let Some(txs) = full_txs {
let _ =
tx_graph.apply_update(txs.clone().map_anchors(|a| ConfirmationHeightAnchor {
anchor_block: a.anchor_block(),
confirmation_height: a.confirmation_height_upper_bound(),
}));
}
let cps = tip let cps = tip
.iter() .iter()
.take(10) .take(10)
@ -108,7 +87,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
scanned_spks.append(&mut populate_with_spks( scanned_spks.append(&mut populate_with_spks(
self, self,
&cps, &cps,
&mut tx_graph, &mut request.tx_cache,
&mut graph_update,
&mut scanned_spks &mut scanned_spks
.iter() .iter()
.map(|(i, (spk, _))| (i.clone(), spk.clone())), .map(|(i, (spk, _))| (i.clone(), spk.clone())),
@ -121,7 +101,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
populate_with_spks( populate_with_spks(
self, self,
&cps, &cps,
&mut tx_graph, &mut request.tx_cache,
&mut graph_update,
keychain_spks, keychain_spks,
stop_gap, stop_gap,
batch_size, batch_size,
@ -140,8 +121,6 @@ impl<E: ElectrumApi> ElectrumExt for E {
let chain_update = tip; let chain_update = tip;
let graph_update = into_confirmation_time_tx_graph(self, &tx_graph)?;
let keychain_update = request_spks let keychain_update = request_spks
.into_keys() .into_keys()
.filter_map(|k| { .filter_map(|k| {
@ -153,41 +132,29 @@ impl<E: ElectrumApi> ElectrumExt for E {
}) })
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
break ( break FullScanResult {
ElectrumUpdate { graph_update,
chain_update, chain_update,
graph_update, last_active_indices: keychain_update,
}, };
keychain_update,
);
}; };
Ok((electrum_update, keychain_update)) Ok(update)
} }
fn sync<A: Anchor>( fn sync(
&self, &self,
prev_tip: CheckPoint, request: SyncRequest,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
full_txs: Option<&TxGraph<A>>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
batch_size: usize, batch_size: usize,
) -> Result<ElectrumUpdate, Error> { ) -> Result<SyncResult<ConfirmationHeightAnchor>, Error> {
let spk_iter = misc_spks let mut tx_cache = request.tx_cache.clone();
.into_iter()
.enumerate()
.map(|(i, spk)| (i as u32, spk));
let (mut electrum_update, _) = self.full_scan( let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
prev_tip.clone(), .cache_txs(request.tx_cache)
[((), spk_iter)].into(), .set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
full_txs, let full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size)?;
usize::MAX,
batch_size,
)?;
let (tip, _) = construct_update_tip(self, prev_tip)?; let (tip, _) = construct_update_tip(self, request.chain_tip)?;
let cps = tip let cps = tip
.iter() .iter()
.take(10) .take(10)
@ -195,16 +162,88 @@ impl<E: ElectrumApi> ElectrumExt for E {
.collect::<BTreeMap<u32, CheckPoint>>(); .collect::<BTreeMap<u32, CheckPoint>>();
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default(); let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
populate_with_txids(self, &cps, &mut tx_graph, txids)?; populate_with_txids(self, &cps, &mut tx_cache, &mut tx_graph, request.txids)?;
populate_with_outpoints(self, &cps, &mut tx_graph, outpoints)?; populate_with_outpoints(self, &cps, &mut tx_cache, &mut tx_graph, request.outpoints)?;
let _ = electrum_update
.graph_update
.apply_update(into_confirmation_time_tx_graph(self, &tx_graph)?);
Ok(electrum_update) Ok(SyncResult {
chain_update: full_scan_res.chain_update,
graph_update: full_scan_res.graph_update,
})
} }
} }
/// Trait that extends [`SyncResult`] and [`FullScanResult`] functionality.
///
/// Currently, only a single method exists that converts the update [`TxGraph`] to have an anchor
/// type of [`ConfirmationTimeHeightAnchor`].
pub trait ElectrumResultExt {
/// New result type with a [`TxGraph`] that contains the [`ConfirmationTimeHeightAnchor`].
type NewResult;
/// Convert result type to have an update [`TxGraph`] that contains the [`ConfirmationTimeHeightAnchor`] .
fn try_into_confirmation_time_result(
self,
client: &impl ElectrumApi,
) -> Result<Self::NewResult, Error>;
}
impl<K> ElectrumResultExt for FullScanResult<K, ConfirmationHeightAnchor> {
type NewResult = FullScanResult<K, ConfirmationTimeHeightAnchor>;
fn try_into_confirmation_time_result(
self,
client: &impl ElectrumApi,
) -> Result<Self::NewResult, Error> {
Ok(FullScanResult::<K, ConfirmationTimeHeightAnchor> {
graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
chain_update: self.chain_update,
last_active_indices: self.last_active_indices,
})
}
}
impl ElectrumResultExt for SyncResult<ConfirmationHeightAnchor> {
type NewResult = SyncResult<ConfirmationTimeHeightAnchor>;
fn try_into_confirmation_time_result(
self,
client: &impl ElectrumApi,
) -> Result<Self::NewResult, Error> {
Ok(SyncResult {
graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
chain_update: self.chain_update,
})
}
}
fn try_into_confirmation_time_result(
graph_update: TxGraph<ConfirmationHeightAnchor>,
client: &impl ElectrumApi,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let relevant_heights = graph_update
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height)
.collect::<HashSet<_>>();
let height_to_time = relevant_heights
.clone()
.into_iter()
.zip(
client
.batch_block_header(relevant_heights)?
.into_iter()
.map(|bh| bh.time as u64),
)
.collect::<HashMap<u32, u64>>();
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
anchor_block: a.anchor_block,
confirmation_height: a.confirmation_height,
confirmation_time: height_to_time[&a.confirmation_height],
}))
}
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip( fn construct_update_tip(
client: &impl ElectrumApi, client: &impl ElectrumApi,
@ -323,6 +362,7 @@ fn determine_tx_anchor(
fn populate_with_outpoints( fn populate_with_outpoints(
client: &impl ElectrumApi, client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>, cps: &BTreeMap<u32, CheckPoint>,
tx_cache: &mut TxCache,
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>, tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -358,9 +398,9 @@ fn populate_with_outpoints(
let res_tx = match tx_graph.get_tx(res.tx_hash) { let res_tx = match tx_graph.get_tx(res.tx_hash) {
Some(tx) => tx, Some(tx) => tx,
None => { None => {
let res_tx = client.transaction_get(&res.tx_hash)?; let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
let _ = tx_graph.insert_tx(res_tx); let _ = tx_graph.insert_tx(Arc::clone(&res_tx));
tx_graph.get_tx(res.tx_hash).expect("just inserted") res_tx
} }
}; };
has_spending = res_tx has_spending = res_tx
@ -383,11 +423,12 @@ fn populate_with_outpoints(
fn populate_with_txids( fn populate_with_txids(
client: &impl ElectrumApi, client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>, cps: &BTreeMap<u32, CheckPoint>,
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>, tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
) -> Result<(), Error> { ) -> Result<(), Error> {
for txid in txids { for txid in txids {
let tx = match client.transaction_get(&txid) { let tx = match fetch_tx(client, tx_cache, txid) {
Ok(tx) => tx, Ok(tx) => tx,
Err(electrum_client::Error::Protocol(_)) => continue, Err(electrum_client::Error::Protocol(_)) => continue,
Err(other_err) => return Err(other_err), Err(other_err) => return Err(other_err),
@ -408,20 +449,36 @@ fn populate_with_txids(
None => continue, None => continue,
}; };
if tx_graph.get_tx(txid).is_none() { if graph_update.get_tx(txid).is_none() {
let _ = tx_graph.insert_tx(tx); // TODO: We need to be able to insert an `Arc` of a transaction.
let _ = graph_update.insert_tx(tx);
} }
if let Some(anchor) = anchor { if let Some(anchor) = anchor {
let _ = tx_graph.insert_anchor(txid, anchor); let _ = graph_update.insert_anchor(txid, anchor);
} }
} }
Ok(()) Ok(())
} }
fn fetch_tx<C: ElectrumApi>(
client: &C,
tx_cache: &mut TxCache,
txid: Txid,
) -> Result<Arc<Transaction>, Error> {
use bdk_chain::collections::hash_map::Entry;
Ok(match tx_cache.entry(txid) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => entry
.insert(Arc::new(client.transaction_get(&txid)?))
.clone(),
})
}
fn populate_with_spks<I: Ord + Clone>( fn populate_with_spks<I: Ord + Clone>(
client: &impl ElectrumApi, client: &impl ElectrumApi,
cps: &BTreeMap<u32, CheckPoint>, cps: &BTreeMap<u32, CheckPoint>,
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>, tx_cache: &mut TxCache,
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
spks: &mut impl Iterator<Item = (I, ScriptBuf)>, spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
stop_gap: usize, stop_gap: usize,
batch_size: usize, batch_size: usize,
@ -453,51 +510,12 @@ fn populate_with_spks<I: Ord + Clone>(
unused_spk_count = 0; unused_spk_count = 0;
} }
for tx in spk_history { for tx_res in spk_history {
let mut update = TxGraph::<ConfirmationHeightAnchor>::default(); let _ = graph_update.insert_tx(fetch_tx(client, tx_cache, tx_res.tx_hash)?);
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
if tx_graph.get_tx(tx.tx_hash).is_none() { let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
let full_tx = client.transaction_get(&tx.tx_hash)?;
update = TxGraph::<ConfirmationHeightAnchor>::new([full_tx]);
} }
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
let _ = update.insert_anchor(tx.tx_hash, anchor);
}
let _ = tx_graph.apply_update(update);
} }
} }
} }
} }
fn into_confirmation_time_tx_graph(
client: &impl ElectrumApi,
tx_graph: &TxGraph<ConfirmationHeightAnchor>,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let relevant_heights = tx_graph
.all_anchors()
.iter()
.map(|(a, _)| a.confirmation_height)
.collect::<HashSet<_>>();
let height_to_time = relevant_heights
.clone()
.into_iter()
.zip(
client
.batch_block_header(relevant_heights)?
.into_iter()
.map(|bh| bh.time as u64),
)
.collect::<HashMap<u32, u64>>();
let new_graph = tx_graph
.clone()
.map_anchors(|a| ConfirmationTimeHeightAnchor {
anchor_block: a.anchor_block,
confirmation_height: a.confirmation_height,
confirmation_time: height_to_time[&a.confirmation_height],
});
Ok(new_graph)
}

View File

@ -2,9 +2,10 @@ use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
keychain::Balance, keychain::Balance,
local_chain::LocalChain, local_chain::LocalChain,
spk_client::SyncRequest,
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex, ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
}; };
use bdk_electrum::{ElectrumExt, ElectrumUpdate}; use bdk_electrum::{ElectrumExt, ElectrumResultExt};
use bdk_testenv::{anyhow, anyhow::Result, bitcoincore_rpc::RpcApi, TestEnv}; use bdk_testenv::{anyhow, anyhow::Result, bitcoincore_rpc::RpcApi, TestEnv};
fn get_balance( fn get_balance(
@ -60,22 +61,18 @@ fn scan_detects_confirmed_tx() -> Result<()> {
// Sync up to tip. // Sync up to tip.
env.wait_until_electrum_sees_block()?; env.wait_until_electrum_sees_block()?;
let ElectrumUpdate { let update = client
chain_update, .sync(
graph_update, SyncRequest::from_chain_tip(recv_chain.tip())
} = client.sync::<ConfirmationTimeHeightAnchor>( .chain_spks(core::iter::once(spk_to_track)),
recv_chain.tip(), 5,
[spk_to_track], )?
Some(recv_graph.graph()), .try_into_confirmation_time_result(&client)?;
None,
None,
5,
)?;
let _ = recv_chain let _ = recv_chain
.apply_update(chain_update) .apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(graph_update); let _ = recv_graph.apply_update(update.graph_update);
// Check to see if tx is confirmed. // Check to see if tx is confirmed.
assert_eq!( assert_eq!(
@ -131,25 +128,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
// Sync up to tip. // Sync up to tip.
env.wait_until_electrum_sees_block()?; env.wait_until_electrum_sees_block()?;
let ElectrumUpdate { let update = client
chain_update, .sync(
graph_update, SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
} = client.sync::<ConfirmationTimeHeightAnchor>( 5,
recv_chain.tip(), )?
[spk_to_track.clone()], .try_into_confirmation_time_result(&client)?;
Some(recv_graph.graph()),
None,
None,
5,
)?;
let _ = recv_chain let _ = recv_chain
.apply_update(chain_update) .apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
let _ = recv_graph.apply_update(graph_update.clone()); let _ = recv_graph.apply_update(update.graph_update.clone());
// Retain a snapshot of all anchors before reorg process. // Retain a snapshot of all anchors before reorg process.
let initial_anchors = graph_update.all_anchors(); let initial_anchors = update.graph_update.all_anchors();
// Check if initial balance is correct. // Check if initial balance is correct.
assert_eq!( assert_eq!(
@ -166,27 +158,22 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
env.reorg_empty_blocks(depth)?; env.reorg_empty_blocks(depth)?;
env.wait_until_electrum_sees_block()?; env.wait_until_electrum_sees_block()?;
let ElectrumUpdate { let update = client
chain_update, .sync(
graph_update, SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
} = client.sync::<ConfirmationTimeHeightAnchor>( 5,
recv_chain.tip(), )?
[spk_to_track.clone()], .try_into_confirmation_time_result(&client)?;
Some(recv_graph.graph()),
None,
None,
5,
)?;
let _ = recv_chain let _ = recv_chain
.apply_update(chain_update) .apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
// Check to see if a new anchor is added during current reorg. // Check to see if a new anchor is added during current reorg.
if !initial_anchors.is_superset(graph_update.all_anchors()) { if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
println!("New anchor added at reorg depth {}", depth); println!("New anchor added at reorg depth {}", depth);
} }
let _ = recv_graph.apply_update(graph_update); let _ = recv_graph.apply_update(update.graph_update);
assert_eq!( assert_eq!(
get_balance(&recv_chain, &recv_graph)?, get_balance(&recv_chain, &recv_graph)?,

View File

@ -1,19 +1,20 @@
use std::{ use std::{
collections::BTreeMap,
io::{self, Write}, io::{self, Write},
sync::Mutex, sync::Mutex,
}; };
use bdk_chain::{ use bdk_chain::{
bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid}, bitcoin::{constants::genesis_block, Address, Network, Txid},
collections::BTreeSet,
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
keychain, keychain,
local_chain::{self, LocalChain}, local_chain::{self, LocalChain},
spk_client::{FullScanRequest, SyncRequest},
Append, ConfirmationHeightAnchor, Append, ConfirmationHeightAnchor,
}; };
use bdk_electrum::{ use bdk_electrum::{
electrum_client::{self, Client, ElectrumApi}, electrum_client::{self, Client, ElectrumApi},
ElectrumExt, ElectrumUpdate, ElectrumExt,
}; };
use example_cli::{ use example_cli::{
anyhow::{self, Context}, anyhow::{self, Context},
@ -147,48 +148,55 @@ fn main() -> anyhow::Result<()> {
let client = electrum_cmd.electrum_args().client(args.network)?; let client = electrum_cmd.electrum_args().client(args.network)?;
let response = match electrum_cmd.clone() { let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
ElectrumCommands::Scan { ElectrumCommands::Scan {
stop_gap, stop_gap,
scan_options, scan_options,
.. ..
} => { } => {
let (keychain_spks, tip) = { let request = {
let graph = &*graph.lock().unwrap(); let graph = &*graph.lock().unwrap();
let chain = &*chain.lock().unwrap(); let chain = &*chain.lock().unwrap();
let keychain_spks = graph FullScanRequest::from_chain_tip(chain.tip())
.index .cache_graph_txs(graph.graph())
.all_unbounded_spk_iters() .set_spks_for_keychain(
.into_iter() Keychain::External,
.map(|(keychain, iter)| { graph
let mut first = true; .index
let spk_iter = iter.inspect(move |(i, _)| { .unbounded_spk_iter(&Keychain::External)
if first { .into_iter()
eprint!("\nscanning {}: ", keychain); .flatten(),
first = false; )
.set_spks_for_keychain(
Keychain::Internal,
graph
.index
.unbounded_spk_iter(&Keychain::Internal)
.into_iter()
.flatten(),
)
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::new();
move |k, spk_i, _| {
if once.insert(k) {
eprint!("\nScanning {}: ", k);
} else {
eprint!("{} ", spk_i);
} }
eprint!("{} ", i);
let _ = io::stdout().flush(); let _ = io::stdout().flush();
}); }
(keychain, spk_iter)
}) })
.collect::<BTreeMap<_, _>>();
let tip = chain.tip();
(keychain_spks, tip)
}; };
client let res = client
.full_scan::<_, ConfirmationHeightAnchor>( .full_scan::<_>(request, stop_gap, scan_options.batch_size)
tip, .context("scanning the blockchain")?;
keychain_spks, (
Some(graph.lock().unwrap().graph()), res.chain_update,
stop_gap, res.graph_update,
scan_options.batch_size, Some(res.last_active_indices),
) )
.context("scanning the blockchain")?
} }
ElectrumCommands::Sync { ElectrumCommands::Sync {
mut unused_spks, mut unused_spks,
@ -201,7 +209,6 @@ fn main() -> anyhow::Result<()> {
// Get a short lock on the tracker to get the spks we're interested in // Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap(); let graph = graph.lock().unwrap();
let chain = chain.lock().unwrap(); let chain = chain.lock().unwrap();
let chain_tip = chain.tip().block_id();
if !(all_spks || unused_spks || utxos || unconfirmed) { if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true; unused_spks = true;
@ -211,18 +218,20 @@ fn main() -> anyhow::Result<()> {
unused_spks = false; unused_spks = false;
} }
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> = let chain_tip = chain.tip();
Box::new(core::iter::empty()); let mut request =
SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(graph.graph());
if all_spks { if all_spks {
let all_spks = graph let all_spks = graph
.index .index
.revealed_spks(..) .revealed_spks(..)
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned())) .map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| { request = request.chain_spks(all_spks.into_iter().map(|(k, spk_i, spk)| {
eprintln!("scanning {}:{}", k, i); eprintln!("scanning {}: {}", k, spk_i);
spk spk
}))); }));
} }
if unused_spks { if unused_spks {
let unused_spks = graph let unused_spks = graph
@ -230,82 +239,61 @@ fn main() -> anyhow::Result<()> {
.unused_spks() .unused_spks()
.map(|(k, i, spk)| (k, i, spk.to_owned())) .map(|(k, i, spk)| (k, i, spk.to_owned()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| { request =
eprintln!( request.chain_spks(unused_spks.into_iter().map(move |(k, spk_i, spk)| {
"Checking if address {} {}:{} has been used", eprintln!(
Address::from_script(&spk, args.network).unwrap(), "Checking if address {} {}:{} has been used",
k, Address::from_script(&spk, args.network).unwrap(),
i, k,
); spk_i,
spk );
}))); spk
}));
} }
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
if utxos { if utxos {
let init_outpoints = graph.index.outpoints(); let init_outpoints = graph.index.outpoints();
let utxos = graph let utxos = graph
.graph() .graph()
.filter_chain_unspents(&*chain, chain_tip, init_outpoints) .filter_chain_unspents(&*chain, chain_tip.block_id(), init_outpoints)
.map(|(_, utxo)| utxo) .map(|(_, utxo)| utxo)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
outpoints = Box::new( eprintln!(
utxos "Checking if outpoint {} (value: {}) has been spent",
.into_iter() utxo.outpoint, utxo.txout.value
.inspect(|utxo| { );
eprintln!( utxo.outpoint
"Checking if outpoint {} (value: {}) has been spent", }));
utxo.outpoint, utxo.txout.value
);
})
.map(|utxo| utxo.outpoint),
);
}; };
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
if unconfirmed { if unconfirmed {
let unconfirmed_txids = graph let unconfirmed_txids = graph
.graph() .graph()
.list_chain_txs(&*chain, chain_tip) .list_chain_txs(&*chain, chain_tip.block_id())
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid) .map(|canonical_tx| canonical_tx.tx_node.txid)
.collect::<Vec<Txid>>(); .collect::<Vec<Txid>>();
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| { request = request.chain_txids(
eprintln!("Checking if {} is confirmed yet", txid); unconfirmed_txids
})); .into_iter()
.inspect(|txid| eprintln!("Checking if {} is confirmed yet", txid)),
);
} }
let electrum_update = client let res = client
.sync::<ConfirmationHeightAnchor>( .sync(request, scan_options.batch_size)
chain.tip(),
spks,
Some(graph.graph()),
txids,
outpoints,
scan_options.batch_size,
)
.context("scanning the blockchain")?; .context("scanning the blockchain")?;
// drop lock on graph and chain // drop lock on graph and chain
drop((graph, chain)); drop((graph, chain));
(electrum_update, BTreeMap::new()) (res.chain_update, res.graph_update, None)
} }
}; };
let (
ElectrumUpdate {
chain_update,
mut graph_update,
},
keychain_update,
) = response;
let now = std::time::UNIX_EPOCH let now = std::time::UNIX_EPOCH
.elapsed() .elapsed()
.expect("must get time") .expect("must get time")
@ -316,26 +304,17 @@ fn main() -> anyhow::Result<()> {
let mut chain = chain.lock().unwrap(); let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap(); let mut graph = graph.lock().unwrap();
let chain = chain.apply_update(chain_update)?; let chain_changeset = chain.apply_update(chain_update)?;
let indexed_tx_graph = { let mut indexed_tx_graph_changeset =
let mut changeset = indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default(); if let Some(keychain_update) = keychain_update {
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update); let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update);
changeset.append(indexed_tx_graph::ChangeSet { indexed_tx_graph_changeset.append(keychain_changeset.into());
indexer, }
..Default::default() indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
});
changeset.append(graph.apply_update(graph_update.map_anchors(|a| {
ConfirmationHeightAnchor {
anchor_block: a.anchor_block,
confirmation_height: a.confirmation_height,
}
})));
changeset
};
(chain, indexed_tx_graph) (chain_changeset, indexed_tx_graph_changeset)
}; };
let mut db = db.lock().unwrap(); let mut db = db.lock().unwrap();

View File

@ -3,17 +3,16 @@ const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 50; const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5; const BATCH_SIZE: usize = 5;
use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use bdk::bitcoin::{Address, Amount}; use bdk::bitcoin::{Address, Amount};
use bdk::chain::ConfirmationTimeHeightAnchor; use bdk::chain::collections::HashSet;
use bdk::wallet::Update;
use bdk::{bitcoin::Network, Wallet}; use bdk::{bitcoin::Network, Wallet};
use bdk::{KeychainKind, SignOptions}; use bdk::{KeychainKind, SignOptions};
use bdk_electrum::ElectrumResultExt;
use bdk_electrum::{ use bdk_electrum::{
electrum_client::{self, ElectrumApi}, electrum_client::{self, ElectrumApi},
ElectrumExt, ElectrumUpdate, ElectrumExt,
}; };
use bdk_file_store::Store; use bdk_file_store::Store;
@ -39,48 +38,24 @@ fn main() -> Result<(), anyhow::Error> {
print!("Syncing..."); print!("Syncing...");
let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?; let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?;
let prev_tip = wallet.latest_checkpoint(); let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
let keychain_spks = wallet let mut once = HashSet::<KeychainKind>::new();
.all_unbounded_spk_iters() move |k, spk_i, _| match once.insert(k) {
.into_iter() true => print!("\nScanning keychain [{:?}]", k),
.map(|(k, k_spks)| { false => print!(" {:<3}", spk_i),
let mut once = Some(()); }
let mut stdout = std::io::stdout(); });
let k_spks = k_spks
.inspect(move |(spk_i, _)| match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", k),
None => print!(" {:<3}", spk_i),
})
.inspect(move |_| stdout.flush().expect("must flush"));
(k, k_spks)
})
.collect();
let ( let mut update = client
ElectrumUpdate { .full_scan(request, STOP_GAP, BATCH_SIZE)?
chain_update, .try_into_confirmation_time_result(&client)?;
mut graph_update,
}, let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
keychain_update, let _ = update.graph_update.update_last_seen_unconfirmed(now);
) = client.full_scan::<_, ConfirmationTimeHeightAnchor>(
prev_tip,
keychain_spks,
Some(wallet.as_ref()),
STOP_GAP,
BATCH_SIZE,
)?;
println!(); println!();
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); wallet.apply_update(update)?;
let _ = graph_update.update_last_seen_unconfirmed(now);
let wallet_update = Update {
last_active_indices: keychain_update,
graph: graph_update,
chain: Some(chain_update),
};
wallet.apply_update(wallet_update)?;
wallet.commit()?; wallet.commit()?;
let balance = wallet.get_balance(); let balance = wallet.get_balance();