Merge bitcoindevkit/bdk#1489: feat(electrum)!: Update bdk_electrum
to use merkle proofs
1a62488abfd6213c2780c1465aebe01dfbb6b20a feat(chain)!: Implement `ConfirmationBlockTime` (Wei Chen) e761adf48178e2688a817a2b7466b0ebf7902eeb test(electrum): Imported `bdk_esplora` tests into `bdk_electrum` (Wei Chen) d7f4ab71e22ca3e8868dae22add8839fc25aa7f3 feat(electrum)!: Update `bdk_electrum` to use merkle proofs (Wei Chen) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> Fixes #980. ### Description This PR is the first step in reworking `bdk_electrum` to use merkle proofs. When we fetch a transaction, we now also obtain the merkle proof and block header for verification. We then insert an anchor only after validation that the transaction exists in a confirmed block. The loop logic that previously existed in `full_scan` to account for re-orgs has also been removed as part of this rework. This is a breaking change because `graph_update`s now provide the full `ConfirmationTimeHeightAnchor` type. This removes the need for the `ElectrumFullScanResult` and `ElectrumSyncResult` structs that existed only to provide the option for converting the anchor type from `ConfirmationHeightAnchor` into `ConfirmationTimeHeightAnchor`. ### Notes to the reviewers <!-- In this section you can include notes directed to the reviewers, like explaining why some parts of the PR were done in a specific way --> ### Changelog notice <!-- Notice the release manager should include in the release tag message changelog --> <!-- See https://keepachangelog.com/en/1.0.0/ for examples --> * `ConfirmationTimeHeightAnchor` and `ConfirmationHeightAnchor` have been removed. * `ConfirmationBlockTime` has been introduced as a new anchor type. * `bdk_electrum`'s `full_scan` and `sync` now return `graph_update`s with `ConfirmationBlockTime`. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [ ] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: ValuedMammal: ACK 1a62488abfd6213c2780c1465aebe01dfbb6b20a notmandatory: ACK 1a62488abfd6213c2780c1465aebe01dfbb6b20a Tree-SHA512: 77af05bffcb9668ecb99b41abacc6b6aa503dc559226fa88c4cab6863e3af431b937706696ec765bb802c9c152333cd430c284d17a6cd190520e10b13d89e02f
This commit is contained in:
commit
d99b3ef4b4
@ -74,11 +74,11 @@ impl ConfirmationTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
|
impl From<ChainPosition<ConfirmationBlockTime>> for ConfirmationTime {
|
||||||
fn from(observed_as: ChainPosition<ConfirmationTimeHeightAnchor>) -> Self {
|
fn from(observed_as: ChainPosition<ConfirmationBlockTime>) -> Self {
|
||||||
match observed_as {
|
match observed_as {
|
||||||
ChainPosition::Confirmed(a) => Self::Confirmed {
|
ChainPosition::Confirmed(a) => Self::Confirmed {
|
||||||
height: a.confirmation_height,
|
height: a.block_id.height,
|
||||||
time: a.confirmation_time,
|
time: a.confirmation_time,
|
||||||
},
|
},
|
||||||
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
||||||
@ -145,9 +145,7 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
|
/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction.
|
||||||
///
|
|
||||||
/// Note that the confirmation block and the anchor block can be different here.
|
|
||||||
///
|
///
|
||||||
/// Refer to [`Anchor`] for more details.
|
/// Refer to [`Anchor`] for more details.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||||
@ -156,70 +154,27 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
|||||||
derive(serde::Deserialize, serde::Serialize),
|
derive(serde::Deserialize, serde::Serialize),
|
||||||
serde(crate = "serde_crate")
|
serde(crate = "serde_crate")
|
||||||
)]
|
)]
|
||||||
pub struct ConfirmationHeightAnchor {
|
pub struct ConfirmationBlockTime {
|
||||||
/// The exact confirmation height of the transaction.
|
|
||||||
///
|
|
||||||
/// It is assumed that this value is never larger than the height of the anchor block.
|
|
||||||
pub confirmation_height: u32,
|
|
||||||
/// The anchor block.
|
/// The anchor block.
|
||||||
pub anchor_block: BlockId,
|
pub block_id: BlockId,
|
||||||
}
|
|
||||||
|
|
||||||
impl Anchor for ConfirmationHeightAnchor {
|
|
||||||
fn anchor_block(&self) -> BlockId {
|
|
||||||
self.anchor_block
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
|
||||||
self.confirmation_height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
|
||||||
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
anchor_block: block_id,
|
|
||||||
confirmation_height: block_id.height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
|
||||||
/// transaction.
|
|
||||||
///
|
|
||||||
/// Note that the confirmation block and the anchor block can be different here.
|
|
||||||
///
|
|
||||||
/// Refer to [`Anchor`] for more details.
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
|
||||||
#[cfg_attr(
|
|
||||||
feature = "serde",
|
|
||||||
derive(serde::Deserialize, serde::Serialize),
|
|
||||||
serde(crate = "serde_crate")
|
|
||||||
)]
|
|
||||||
pub struct ConfirmationTimeHeightAnchor {
|
|
||||||
/// The confirmation height of the transaction being anchored.
|
|
||||||
pub confirmation_height: u32,
|
|
||||||
/// The confirmation time of the transaction being anchored.
|
/// The confirmation time of the transaction being anchored.
|
||||||
pub confirmation_time: u64,
|
pub confirmation_time: u64,
|
||||||
/// The anchor block.
|
|
||||||
pub anchor_block: BlockId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
impl Anchor for ConfirmationBlockTime {
|
||||||
fn anchor_block(&self) -> BlockId {
|
fn anchor_block(&self) -> BlockId {
|
||||||
self.anchor_block
|
self.block_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||||
self.confirmation_height
|
self.block_id.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
impl AnchorFromBlockPosition for ConfirmationBlockTime {
|
||||||
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
anchor_block: block_id,
|
block_id,
|
||||||
confirmation_height: block_id.height,
|
|
||||||
confirmation_time: block.header.time as _,
|
confirmation_time: block.header.time as _,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,19 +260,19 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_position_ord() {
|
fn chain_position_ord() {
|
||||||
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
|
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
|
||||||
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
|
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
|
||||||
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
|
||||||
confirmation_height: 9,
|
confirmation_time: 20,
|
||||||
anchor_block: BlockId {
|
block_id: BlockId {
|
||||||
height: 20,
|
height: 9,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
|
||||||
confirmation_height: 12,
|
confirmation_time: 15,
|
||||||
anchor_block: BlockId {
|
block_id: BlockId {
|
||||||
height: 15,
|
height: 12,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Helper types for spk-based blockchain clients.
|
//! Helper types for spk-based blockchain clients.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collections::BTreeMap, local_chain::CheckPoint, ConfirmationTimeHeightAnchor, Indexed, TxGraph,
|
collections::BTreeMap, local_chain::CheckPoint, ConfirmationBlockTime, Indexed, TxGraph,
|
||||||
};
|
};
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
|
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
|
||||||
@ -176,7 +176,7 @@ impl SyncRequest {
|
|||||||
/// Data returned from a spk-based blockchain client sync.
|
/// Data returned from a spk-based blockchain client sync.
|
||||||
///
|
///
|
||||||
/// See also [`SyncRequest`].
|
/// See also [`SyncRequest`].
|
||||||
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
|
pub struct SyncResult<A = ConfirmationBlockTime> {
|
||||||
/// The update to apply to the receiving [`TxGraph`].
|
/// The update to apply to the receiving [`TxGraph`].
|
||||||
pub graph_update: TxGraph<A>,
|
pub graph_update: TxGraph<A>,
|
||||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||||
@ -317,7 +317,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
|
|||||||
/// Data returned from a spk-based blockchain client full scan.
|
/// Data returned from a spk-based blockchain client full scan.
|
||||||
///
|
///
|
||||||
/// See also [`FullScanRequest`].
|
/// See also [`FullScanRequest`].
|
||||||
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
|
pub struct FullScanResult<K, A = ConfirmationBlockTime> {
|
||||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||||
pub graph_update: TxGraph<A>,
|
pub graph_update: TxGraph<A>,
|
||||||
/// The update to apply to the receiving [`TxGraph`].
|
/// The update to apply to the receiving [`TxGraph`].
|
||||||
|
@ -20,8 +20,7 @@ use alloc::vec::Vec;
|
|||||||
/// # use bdk_chain::local_chain::LocalChain;
|
/// # use bdk_chain::local_chain::LocalChain;
|
||||||
/// # use bdk_chain::tx_graph::TxGraph;
|
/// # use bdk_chain::tx_graph::TxGraph;
|
||||||
/// # use bdk_chain::BlockId;
|
/// # use bdk_chain::BlockId;
|
||||||
/// # use bdk_chain::ConfirmationHeightAnchor;
|
/// # use bdk_chain::ConfirmationBlockTime;
|
||||||
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
|
|
||||||
/// # use bdk_chain::example_utils::*;
|
/// # use bdk_chain::example_utils::*;
|
||||||
/// # use bitcoin::hashes::Hash;
|
/// # use bitcoin::hashes::Hash;
|
||||||
/// // Initialize the local chain with two blocks.
|
/// // Initialize the local chain with two blocks.
|
||||||
@ -50,39 +49,19 @@ use alloc::vec::Vec;
|
|||||||
/// },
|
/// },
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
|
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationBlockTime` as the anchor type.
|
||||||
/// // This anchor records the anchor block and the confirmation height of the transaction.
|
/// // This anchor records the anchor block and the confirmation time of the transaction. When a
|
||||||
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
|
/// // transaction is anchored with `ConfirmationBlockTime`, the anchor block and confirmation block
|
||||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
/// // of the transaction is the same block.
|
||||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
/// let mut graph_c = TxGraph::<ConfirmationBlockTime>::default();
|
||||||
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
|
|
||||||
/// let _ = graph_b.insert_tx(tx.clone());
|
|
||||||
/// graph_b.insert_anchor(
|
|
||||||
/// tx.compute_txid(),
|
|
||||||
/// ConfirmationHeightAnchor {
|
|
||||||
/// anchor_block: BlockId {
|
|
||||||
/// height: 2,
|
|
||||||
/// hash: Hash::hash("second".as_bytes()),
|
|
||||||
/// },
|
|
||||||
/// confirmation_height: 1,
|
|
||||||
/// },
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
|
|
||||||
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
|
|
||||||
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
|
|
||||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
|
||||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
|
||||||
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
|
||||||
/// let _ = graph_c.insert_tx(tx.clone());
|
/// let _ = graph_c.insert_tx(tx.clone());
|
||||||
/// graph_c.insert_anchor(
|
/// graph_c.insert_anchor(
|
||||||
/// tx.compute_txid(),
|
/// tx.compute_txid(),
|
||||||
/// ConfirmationTimeHeightAnchor {
|
/// ConfirmationBlockTime {
|
||||||
/// anchor_block: BlockId {
|
/// block_id: BlockId {
|
||||||
/// height: 2,
|
/// height: 2,
|
||||||
/// hash: Hash::hash("third".as_bytes()),
|
/// hash: Hash::hash("third".as_bytes()),
|
||||||
/// },
|
/// },
|
||||||
/// confirmation_height: 1,
|
|
||||||
/// confirmation_time: 123,
|
/// confirmation_time: 123,
|
||||||
/// },
|
/// },
|
||||||
/// );
|
/// );
|
||||||
|
@ -10,7 +10,7 @@ use bdk_chain::{
|
|||||||
indexed_tx_graph::{self, IndexedTxGraph},
|
indexed_tx_graph::{self, IndexedTxGraph},
|
||||||
indexer::keychain_txout::KeychainTxOutIndex,
|
indexer::keychain_txout::KeychainTxOutIndex,
|
||||||
local_chain::LocalChain,
|
local_chain::LocalChain,
|
||||||
tx_graph, Balance, ChainPosition, ConfirmationHeightAnchor, DescriptorExt, Merge,
|
tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge,
|
||||||
};
|
};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||||
@ -32,7 +32,7 @@ fn insert_relevant_txs() {
|
|||||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||||
|
|
||||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
|
||||||
KeychainTxOutIndex::new(10),
|
KeychainTxOutIndex::new(10),
|
||||||
);
|
);
|
||||||
let _ = graph
|
let _ = graph
|
||||||
@ -140,7 +140,7 @@ fn test_list_owned_txouts() {
|
|||||||
let (desc_2, _) =
|
let (desc_2, _) =
|
||||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
|
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
|
||||||
|
|
||||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::new(
|
||||||
KeychainTxOutIndex::new(10),
|
KeychainTxOutIndex::new(10),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -250,9 +250,9 @@ fn test_list_owned_txouts() {
|
|||||||
local_chain
|
local_chain
|
||||||
.get(height)
|
.get(height)
|
||||||
.map(|cp| cp.block_id())
|
.map(|cp| cp.block_id())
|
||||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
.map(|block_id| ConfirmationBlockTime {
|
||||||
anchor_block,
|
block_id,
|
||||||
confirmation_height: anchor_block.height,
|
confirmation_time: 100,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@ -261,8 +261,7 @@ fn test_list_owned_txouts() {
|
|||||||
|
|
||||||
// A helper lambda to extract and filter data from the graph.
|
// A helper lambda to extract and filter data from the graph.
|
||||||
let fetch =
|
let fetch =
|
||||||
|height: u32,
|
|height: u32, graph: &IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<String>>| {
|
||||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
|
||||||
let chain_tip = local_chain
|
let chain_tip = local_chain
|
||||||
.get(height)
|
.get(height)
|
||||||
.map(|cp| cp.block_id())
|
.map(|cp| cp.block_id())
|
||||||
|
@ -7,7 +7,7 @@ use bdk_chain::{
|
|||||||
collections::*,
|
collections::*,
|
||||||
local_chain::LocalChain,
|
local_chain::LocalChain,
|
||||||
tx_graph::{ChangeSet, TxGraph},
|
tx_graph::{ChangeSet, TxGraph},
|
||||||
Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor, Merge,
|
Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge,
|
||||||
};
|
};
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount,
|
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount,
|
||||||
@ -935,7 +935,7 @@ fn test_chain_spends() {
|
|||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||||
|
|
||||||
let _ = graph.insert_tx(tx_0.clone());
|
let _ = graph.insert_tx(tx_0.clone());
|
||||||
let _ = graph.insert_tx(tx_1.clone());
|
let _ = graph.insert_tx(tx_1.clone());
|
||||||
@ -944,9 +944,9 @@ fn test_chain_spends() {
|
|||||||
for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
|
for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
|
||||||
let _ = graph.insert_anchor(
|
let _ = graph.insert_anchor(
|
||||||
tx.compute_txid(),
|
tx.compute_txid(),
|
||||||
ConfirmationHeightAnchor {
|
ConfirmationBlockTime {
|
||||||
anchor_block: tip.block_id(),
|
block_id: tip.get(ht).unwrap().block_id(),
|
||||||
confirmation_height: ht,
|
confirmation_time: 100,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -959,9 +959,12 @@ fn test_chain_spends() {
|
|||||||
OutPoint::new(tx_0.compute_txid(), 0)
|
OutPoint::new(tx_0.compute_txid(), 0)
|
||||||
),
|
),
|
||||||
Some((
|
Some((
|
||||||
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
ChainPosition::Confirmed(&ConfirmationBlockTime {
|
||||||
anchor_block: tip.block_id(),
|
block_id: BlockId {
|
||||||
confirmation_height: 98
|
hash: tip.get(98).unwrap().hash(),
|
||||||
|
height: 98,
|
||||||
|
},
|
||||||
|
confirmation_time: 100
|
||||||
}),
|
}),
|
||||||
tx_1.compute_txid(),
|
tx_1.compute_txid(),
|
||||||
)),
|
)),
|
||||||
@ -971,9 +974,12 @@ fn test_chain_spends() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
|
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
|
||||||
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
|
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
|
||||||
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
|
||||||
anchor_block: tip.block_id(),
|
block_id: BlockId {
|
||||||
confirmation_height: 95
|
hash: tip.get(95).unwrap().hash(),
|
||||||
|
height: 95,
|
||||||
|
},
|
||||||
|
confirmation_time: 100
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid},
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
collections::{BTreeMap, HashMap},
|
||||||
local_chain::CheckPoint,
|
local_chain::CheckPoint,
|
||||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||||
tx_graph::TxGraph,
|
tx_graph::TxGraph,
|
||||||
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
Anchor, BlockId, ConfirmationBlockTime,
|
||||||
};
|
};
|
||||||
use core::str::FromStr;
|
|
||||||
use electrum_client::{ElectrumApi, Error, HeaderNotification};
|
use electrum_client::{ElectrumApi, Error, HeaderNotification};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::{
|
||||||
|
collections::BTreeSet,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
/// 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;
|
||||||
@ -21,6 +23,8 @@ pub struct BdkElectrumClient<E> {
|
|||||||
pub inner: E,
|
pub inner: E,
|
||||||
/// The transaction cache
|
/// The transaction cache
|
||||||
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
|
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
|
||||||
|
/// The header cache
|
||||||
|
block_header_cache: Mutex<HashMap<u32, Header>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: ElectrumApi> BdkElectrumClient<E> {
|
impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||||
@ -29,6 +33,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
Self {
|
Self {
|
||||||
inner: client,
|
inner: client,
|
||||||
tx_cache: Default::default(),
|
tx_cache: Default::default(),
|
||||||
|
block_header_cache: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +70,33 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch block header of given `height`.
|
||||||
|
///
|
||||||
|
/// If it hits the cache it will return the cached version and avoid making the request.
|
||||||
|
fn fetch_header(&self, height: u32) -> Result<Header, Error> {
|
||||||
|
let block_header_cache = self.block_header_cache.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(header) = block_header_cache.get(&height) {
|
||||||
|
return Ok(*header);
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(block_header_cache);
|
||||||
|
|
||||||
|
self.update_header(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a block header at given `height`. Returns the updated header.
|
||||||
|
fn update_header(&self, height: u32) -> Result<Header, Error> {
|
||||||
|
let header = self.inner.block_header(height as usize)?;
|
||||||
|
|
||||||
|
self.block_header_cache
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(height, header);
|
||||||
|
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
/// Broadcasts a transaction to the network.
|
/// Broadcasts a transaction to the network.
|
||||||
///
|
///
|
||||||
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
|
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
|
||||||
@ -88,87 +120,32 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
fetch_prev_txouts: bool,
|
fetch_prev_txouts: bool,
|
||||||
) -> Result<ElectrumFullScanResult<K>, Error> {
|
) -> Result<FullScanResult<K>, Error> {
|
||||||
let mut request_spks = request.spks_by_keychain;
|
let (tip, latest_blocks) =
|
||||||
|
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
|
||||||
|
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
|
||||||
|
let mut last_active_indices = BTreeMap::<K, u32>::new();
|
||||||
|
|
||||||
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
|
for (keychain, spks) in request.spks_by_keychain {
|
||||||
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
|
if let Some(last_active_index) =
|
||||||
// cannot be collected. In addition, we keep track of whether an spk has an active tx
|
self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)?
|
||||||
// history for determining the `last_active_index`.
|
{
|
||||||
// * key: (keychain, spk_index) that identifies the spk.
|
last_active_indices.insert(keychain, last_active_index);
|
||||||
// * val: (script_pubkey, has_tx_history).
|
|
||||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
|
||||||
|
|
||||||
let update = loop {
|
|
||||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
|
|
||||||
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
|
|
||||||
let cps = tip
|
|
||||||
.iter()
|
|
||||||
.take(10)
|
|
||||||
.map(|cp| (cp.height(), cp))
|
|
||||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
|
||||||
|
|
||||||
if !request_spks.is_empty() {
|
|
||||||
if !scanned_spks.is_empty() {
|
|
||||||
scanned_spks.append(
|
|
||||||
&mut self.populate_with_spks(
|
|
||||||
&cps,
|
|
||||||
&mut graph_update,
|
|
||||||
&mut scanned_spks
|
|
||||||
.iter()
|
|
||||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
|
||||||
stop_gap,
|
|
||||||
batch_size,
|
|
||||||
)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (keychain, keychain_spks) in &mut request_spks {
|
|
||||||
scanned_spks.extend(
|
|
||||||
self.populate_with_spks(
|
|
||||||
&cps,
|
|
||||||
&mut graph_update,
|
|
||||||
keychain_spks,
|
|
||||||
stop_gap,
|
|
||||||
batch_size,
|
|
||||||
)?
|
|
||||||
.into_iter()
|
|
||||||
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for reorgs during scan process
|
let chain_update = chain_update(tip, &latest_blocks, graph_update.all_anchors())?;
|
||||||
let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
|
|
||||||
if tip.hash() != server_blockhash {
|
|
||||||
continue; // reorg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||||
if fetch_prev_txouts {
|
if fetch_prev_txouts {
|
||||||
self.fetch_prev_txout(&mut graph_update)?;
|
self.fetch_prev_txout(&mut graph_update)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let chain_update = tip;
|
Ok(FullScanResult {
|
||||||
|
|
||||||
let keychain_update = request_spks
|
|
||||||
.into_keys()
|
|
||||||
.filter_map(|k| {
|
|
||||||
scanned_spks
|
|
||||||
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
|
|
||||||
.rev()
|
|
||||||
.find(|(_, (_, active))| *active)
|
|
||||||
.map(|((_, i), _)| (k, *i))
|
|
||||||
})
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
|
|
||||||
break FullScanResult {
|
|
||||||
graph_update,
|
graph_update,
|
||||||
chain_update,
|
chain_update,
|
||||||
last_active_indices: keychain_update,
|
last_active_indices,
|
||||||
};
|
})
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ElectrumFullScanResult(update))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@ -190,32 +167,31 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
request: SyncRequest,
|
request: SyncRequest,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
fetch_prev_txouts: bool,
|
fetch_prev_txouts: bool,
|
||||||
) -> Result<ElectrumSyncResult, Error> {
|
) -> Result<SyncResult, Error> {
|
||||||
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
|
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
|
||||||
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
|
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
|
||||||
let mut full_scan_res = self
|
let mut full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size, false)?;
|
||||||
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
|
let (tip, latest_blocks) =
|
||||||
.with_confirmation_height_anchor();
|
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
|
||||||
|
|
||||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
|
self.populate_with_txids(&mut full_scan_res.graph_update, request.txids)?;
|
||||||
let cps = tip
|
self.populate_with_outpoints(&mut full_scan_res.graph_update, request.outpoints)?;
|
||||||
.iter()
|
|
||||||
.take(10)
|
|
||||||
.map(|cp| (cp.height(), cp))
|
|
||||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
|
||||||
|
|
||||||
self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
|
let chain_update = chain_update(
|
||||||
self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
|
tip,
|
||||||
|
&latest_blocks,
|
||||||
|
full_scan_res.graph_update.all_anchors(),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||||
if fetch_prev_txouts {
|
if fetch_prev_txouts {
|
||||||
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
|
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ElectrumSyncResult(SyncResult {
|
Ok(SyncResult {
|
||||||
chain_update: full_scan_res.chain_update,
|
chain_update,
|
||||||
graph_update: full_scan_res.graph_update,
|
graph_update: full_scan_res.graph_update,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
|
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
|
||||||
@ -223,84 +199,55 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
/// Transactions that contains an output with requested spk, or spends form an output with
|
/// Transactions that contains an output with requested spk, or spends form an output with
|
||||||
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
|
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
|
||||||
/// also included.
|
/// also included.
|
||||||
///
|
fn populate_with_spks(
|
||||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
|
||||||
fn populate_with_spks<I: Ord + Clone>(
|
|
||||||
&self,
|
&self,
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
mut spks: impl Iterator<Item = (u32, ScriptBuf)>,
|
||||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
batch_size: usize,
|
batch_size: usize,
|
||||||
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
|
) -> Result<Option<u32>, Error> {
|
||||||
let mut unused_spk_count = 0_usize;
|
let mut unused_spk_count = 0_usize;
|
||||||
let mut scanned_spks = BTreeMap::new();
|
let mut last_active_index = Option::<u32>::None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let spks = (0..batch_size)
|
let spks = (0..batch_size)
|
||||||
.map_while(|_| spks.next())
|
.map_while(|_| spks.next())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if spks.is_empty() {
|
if spks.is_empty() {
|
||||||
return Ok(scanned_spks);
|
return Ok(last_active_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
let spk_histories = self
|
let spk_histories = self
|
||||||
.inner
|
.inner
|
||||||
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
||||||
|
|
||||||
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
for ((spk_index, _spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||||
if spk_history.is_empty() {
|
if spk_history.is_empty() {
|
||||||
scanned_spks.insert(spk_index, (spk, false));
|
unused_spk_count = unused_spk_count.saturating_add(1);
|
||||||
unused_spk_count += 1;
|
if unused_spk_count >= stop_gap {
|
||||||
if unused_spk_count > stop_gap {
|
return Ok(last_active_index);
|
||||||
return Ok(scanned_spks);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
scanned_spks.insert(spk_index, (spk, true));
|
last_active_index = Some(spk_index);
|
||||||
unused_spk_count = 0;
|
unused_spk_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for tx_res in spk_history {
|
for tx_res in spk_history {
|
||||||
let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
|
let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
|
||||||
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
|
self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?;
|
||||||
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
|
||||||
// which we do not have by default. This data is needed to calculate the transaction fee.
|
|
||||||
fn fetch_prev_txout(
|
|
||||||
&self,
|
|
||||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let full_txs: Vec<Arc<Transaction>> =
|
|
||||||
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
|
||||||
for tx in full_txs {
|
|
||||||
for vin in &tx.input {
|
|
||||||
let outpoint = vin.previous_output;
|
|
||||||
let vout = outpoint.vout;
|
|
||||||
let prev_tx = self.fetch_tx(outpoint.txid)?;
|
|
||||||
let txout = prev_tx.output[vout as usize].clone();
|
|
||||||
let _ = graph_update.insert_txout(outpoint, txout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
|
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
|
||||||
///
|
///
|
||||||
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
|
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
|
||||||
/// included. Anchors of the aforementioned transactions are included.
|
/// included. Anchors of the aforementioned transactions are included.
|
||||||
///
|
|
||||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
|
||||||
fn populate_with_outpoints(
|
fn populate_with_outpoints(
|
||||||
&self,
|
&self,
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
for outpoint in outpoints {
|
for outpoint in outpoints {
|
||||||
@ -324,9 +271,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
if !has_residing && res.tx_hash == op_txid {
|
if !has_residing && res.tx_hash == op_txid {
|
||||||
has_residing = true;
|
has_residing = true;
|
||||||
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
|
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
|
||||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
|
||||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_spending && res.tx_hash != op_txid {
|
if !has_spending && res.tx_hash != op_txid {
|
||||||
@ -340,9 +285,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
|
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
|
||||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
|
||||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,8 +295,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
|
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
|
||||||
fn populate_with_txids(
|
fn populate_with_txids(
|
||||||
&self,
|
&self,
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||||
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 {
|
||||||
@ -371,120 +313,100 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
|||||||
|
|
||||||
// because of restrictions of the Electrum API, we have to use the `script_get_history`
|
// because of restrictions of the Electrum API, we have to use the `script_get_history`
|
||||||
// call to get confirmation status of our transaction
|
// call to get confirmation status of our transaction
|
||||||
let anchor = match self
|
if let Some(r) = self
|
||||||
.inner
|
.inner
|
||||||
.script_get_history(spk)?
|
.script_get_history(spk)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|r| r.tx_hash == txid)
|
.find(|r| r.tx_hash == txid)
|
||||||
{
|
{
|
||||||
Some(r) => determine_tx_anchor(cps, r.height, txid),
|
self.validate_merkle_for_anchor(graph_update, txid, r.height)?;
|
||||||
None => continue,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let _ = graph_update.insert_tx(tx);
|
let _ = graph_update.insert_tx(tx);
|
||||||
if let Some(anchor) = anchor {
|
}
|
||||||
let _ = graph_update.insert_anchor(txid, anchor);
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function which checks if a transaction is confirmed by validating the merkle proof.
|
||||||
|
// An anchor is inserted if the transaction is validated to be in a confirmed block.
|
||||||
|
fn validate_merkle_for_anchor(
|
||||||
|
&self,
|
||||||
|
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||||
|
txid: Txid,
|
||||||
|
confirmation_height: i32,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Ok(merkle_res) = self
|
||||||
|
.inner
|
||||||
|
.transaction_get_merkle(&txid, confirmation_height as usize)
|
||||||
|
{
|
||||||
|
let mut header = self.fetch_header(merkle_res.block_height as u32)?;
|
||||||
|
let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
|
||||||
|
&txid,
|
||||||
|
&header.merkle_root,
|
||||||
|
&merkle_res,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merkle validation will fail if the header in `block_header_cache` is outdated, so we
|
||||||
|
// want to check if there is a new header and validate against the new one.
|
||||||
|
if !is_confirmed_tx {
|
||||||
|
header = self.update_header(merkle_res.block_height as u32)?;
|
||||||
|
is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
|
||||||
|
&txid,
|
||||||
|
&header.merkle_root,
|
||||||
|
&merkle_res,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_confirmed_tx {
|
||||||
|
let _ = graph_update.insert_anchor(
|
||||||
|
txid,
|
||||||
|
ConfirmationBlockTime {
|
||||||
|
confirmation_time: header.time as u64,
|
||||||
|
block_id: BlockId {
|
||||||
|
height: merkle_res.block_height as u32,
|
||||||
|
hash: header.block_hash(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
||||||
|
// which we do not have by default. This data is needed to calculate the transaction fee.
|
||||||
|
fn fetch_prev_txout(
|
||||||
|
&self,
|
||||||
|
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let full_txs: Vec<Arc<Transaction>> =
|
||||||
|
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
||||||
|
for tx in full_txs {
|
||||||
|
for vin in &tx.input {
|
||||||
|
let outpoint = vin.previous_output;
|
||||||
|
let vout = outpoint.vout;
|
||||||
|
let prev_tx = self.fetch_tx(outpoint.txid)?;
|
||||||
|
let txout = prev_tx.output[vout as usize].clone();
|
||||||
|
let _ = graph_update.insert_txout(outpoint, txout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of [`BdkElectrumClient::full_scan`].
|
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are
|
||||||
///
|
/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
|
||||||
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
|
fn fetch_tip_and_latest_blocks(
|
||||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
|
||||||
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
|
|
||||||
|
|
||||||
impl<K> ElectrumFullScanResult<K> {
|
|
||||||
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
|
|
||||||
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
|
|
||||||
///
|
|
||||||
/// This requires additional calls to the Electrum server.
|
|
||||||
pub fn with_confirmation_time_height_anchor(
|
|
||||||
self,
|
|
||||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
|
||||||
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
|
|
||||||
let res = self.0;
|
|
||||||
Ok(FullScanResult {
|
|
||||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
|
||||||
chain_update: res.chain_update,
|
|
||||||
last_active_indices: res.last_active_indices,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of [`BdkElectrumClient::sync`].
|
|
||||||
///
|
|
||||||
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
|
|
||||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
|
||||||
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
|
|
||||||
|
|
||||||
impl ElectrumSyncResult {
|
|
||||||
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
|
|
||||||
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
|
|
||||||
///
|
|
||||||
/// This requires additional calls to the Electrum server.
|
|
||||||
pub fn with_confirmation_time_height_anchor(
|
|
||||||
self,
|
|
||||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
|
||||||
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
|
|
||||||
let res = self.0;
|
|
||||||
Ok(SyncResult {
|
|
||||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
|
||||||
chain_update: res.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`.
|
|
||||||
fn construct_update_tip(
|
|
||||||
client: &impl ElectrumApi,
|
client: &impl ElectrumApi,
|
||||||
prev_tip: CheckPoint,
|
prev_tip: CheckPoint,
|
||||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
|
||||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||||
let new_tip_height = height as u32;
|
let new_tip_height = height as u32;
|
||||||
|
|
||||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||||
if new_tip_height < prev_tip.height() {
|
if new_tip_height < prev_tip.height() {
|
||||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
return Ok((prev_tip, BTreeMap::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||||
@ -527,10 +449,13 @@ fn construct_update_tip(
|
|||||||
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
||||||
|
|
||||||
let new_tip = new_blocks
|
let new_tip = new_blocks
|
||||||
.into_iter()
|
.iter()
|
||||||
// Prune `new_blocks` to only include blocks that are actually new.
|
// Prune `new_blocks` to only include blocks that are actually new.
|
||||||
.filter(|(height, _)| Some(*height) > agreement_height)
|
.filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height)
|
||||||
.map(|(height, hash)| BlockId { height, hash })
|
.map(|(height, hash)| BlockId {
|
||||||
|
height: *height,
|
||||||
|
hash: *hash,
|
||||||
|
})
|
||||||
.fold(agreement_cp, |prev_cp, block| {
|
.fold(agreement_cp, |prev_cp, block| {
|
||||||
Some(match prev_cp {
|
Some(match prev_cp {
|
||||||
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
||||||
@ -539,51 +464,28 @@ fn construct_update_tip(
|
|||||||
})
|
})
|
||||||
.expect("must have at least one checkpoint");
|
.expect("must have at least one checkpoint");
|
||||||
|
|
||||||
Ok((new_tip, agreement_height))
|
Ok((new_tip, new_blocks))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
|
// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not
|
||||||
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
|
// surpass `latest_blocks`.
|
||||||
///
|
fn chain_update<A: Anchor>(
|
||||||
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
|
mut tip: CheckPoint,
|
||||||
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
|
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||||
///
|
anchors: &BTreeSet<(A, Txid)>,
|
||||||
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
|
) -> Result<CheckPoint, Error> {
|
||||||
fn determine_tx_anchor(
|
for anchor in anchors {
|
||||||
cps: &BTreeMap<u32, CheckPoint>,
|
let height = anchor.0.anchor_block().height;
|
||||||
raw_height: i32,
|
|
||||||
txid: Txid,
|
// Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent
|
||||||
) -> Option<ConfirmationHeightAnchor> {
|
// in case of a re-org.
|
||||||
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
|
if tip.get(height).is_none() && height <= tip.height() {
|
||||||
// height of 0. To avoid invalid representation in our data structures, we manually set
|
let hash = match latest_blocks.get(&height) {
|
||||||
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
|
Some(&hash) => hash,
|
||||||
// unconfirmed for all other transactions.
|
None => anchor.0.anchor_block().hash,
|
||||||
if txid
|
};
|
||||||
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
|
tip = tip.insert(BlockId { hash, height });
|
||||||
.expect("must deserialize genesis coinbase txid")
|
|
||||||
{
|
|
||||||
let anchor_block = cps.values().next()?.block_id();
|
|
||||||
return Some(ConfirmationHeightAnchor {
|
|
||||||
anchor_block,
|
|
||||||
confirmation_height: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
match raw_height {
|
|
||||||
h if h <= 0 => {
|
|
||||||
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
h => {
|
|
||||||
let h = h as u32;
|
|
||||||
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
|
|
||||||
if h > anchor_block.height {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ConfirmationHeightAnchor {
|
|
||||||
anchor_block,
|
|
||||||
confirmation_height: h,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(tip)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
|
||||||
local_chain::LocalChain,
|
local_chain::LocalChain,
|
||||||
spk_client::SyncRequest,
|
spk_client::{FullScanRequest, SyncRequest},
|
||||||
Balance, ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex,
|
||||||
};
|
};
|
||||||
use bdk_electrum::BdkElectrumClient;
|
use bdk_electrum::BdkElectrumClient;
|
||||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||||
|
use std::collections::{BTreeSet, HashSet};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
fn get_balance(
|
fn get_balance(
|
||||||
recv_chain: &LocalChain,
|
recv_chain: &LocalChain,
|
||||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
recv_graph: &IndexedTxGraph<ConfirmationBlockTime, SpkTxOutIndex<()>>,
|
||||||
) -> anyhow::Result<Balance> {
|
) -> anyhow::Result<Balance> {
|
||||||
let chain_tip = recv_chain.tip().block_id();
|
let chain_tip = recv_chain.tip().block_id();
|
||||||
let outpoints = recv_graph.index.outpoints().clone();
|
let outpoints = recv_graph.index.outpoints().clone();
|
||||||
@ -19,6 +21,222 @@ fn get_balance(
|
|||||||
Ok(balance)
|
Ok(balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||||
|
let client = BdkElectrumClient::new(electrum_client);
|
||||||
|
|
||||||
|
let receive_address0 =
|
||||||
|
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||||
|
let receive_address1 =
|
||||||
|
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
|
||||||
|
|
||||||
|
let misc_spks = [
|
||||||
|
receive_address0.script_pubkey(),
|
||||||
|
receive_address1.script_pubkey(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
let txid1 = env.bitcoind.client.send_to_address(
|
||||||
|
&receive_address1,
|
||||||
|
Amount::from_sat(10000),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
let txid2 = env.bitcoind.client.send_to_address(
|
||||||
|
&receive_address0,
|
||||||
|
Amount::from_sat(20000),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
env.mine_blocks(1, None)?;
|
||||||
|
env.wait_until_electrum_sees_block()?;
|
||||||
|
|
||||||
|
// use a full checkpoint linked list (since this is not what we are testing)
|
||||||
|
let cp_tip = env.make_checkpoint_tip();
|
||||||
|
|
||||||
|
let sync_update = {
|
||||||
|
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||||
|
client.sync(request, 1, true)?
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
{
|
||||||
|
let update_cps = sync_update
|
||||||
|
.chain_update
|
||||||
|
.iter()
|
||||||
|
.map(|cp| cp.block_id())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let superset_cps = cp_tip
|
||||||
|
.iter()
|
||||||
|
.map(|cp| cp.block_id())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
superset_cps.is_superset(&update_cps)
|
||||||
|
},
|
||||||
|
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||||
|
);
|
||||||
|
|
||||||
|
let graph_update = sync_update.graph_update;
|
||||||
|
// Check to see if we have the floating txouts available from our two created transactions'
|
||||||
|
// previous outputs in order to calculate transaction fees.
|
||||||
|
for tx in graph_update.full_txs() {
|
||||||
|
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||||
|
// floating txouts available from the transactions' previous outputs.
|
||||||
|
let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
|
||||||
|
|
||||||
|
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||||
|
let tx_fee = env
|
||||||
|
.bitcoind
|
||||||
|
.client
|
||||||
|
.get_transaction(&tx.txid, None)
|
||||||
|
.expect("Tx must exist")
|
||||||
|
.fee
|
||||||
|
.expect("Fee must exist")
|
||||||
|
.abs()
|
||||||
|
.to_unsigned()
|
||||||
|
.expect("valid `Amount`");
|
||||||
|
|
||||||
|
// Check that the calculated fee matches the fee from the transaction data.
|
||||||
|
assert_eq!(fee, tx_fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||||
|
graph_update_txids.sort();
|
||||||
|
let mut expected_txids = vec![txid1, txid2];
|
||||||
|
expected_txids.sort();
|
||||||
|
assert_eq!(graph_update_txids, expected_txids);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||||
|
#[test]
|
||||||
|
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||||
|
let env = TestEnv::new()?;
|
||||||
|
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||||
|
let client = BdkElectrumClient::new(electrum_client);
|
||||||
|
let _block_hashes = env.mine_blocks(101, None)?;
|
||||||
|
|
||||||
|
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||||
|
let addresses = [
|
||||||
|
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
|
||||||
|
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
|
||||||
|
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
|
||||||
|
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
|
||||||
|
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
|
||||||
|
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
|
||||||
|
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
|
||||||
|
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
|
||||||
|
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
|
||||||
|
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
|
||||||
|
];
|
||||||
|
let addresses: Vec<_> = addresses
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| Address::from_str(s).unwrap().assume_checked())
|
||||||
|
.collect();
|
||||||
|
let spks: Vec<_> = addresses
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Then receive coins on the 4th address.
|
||||||
|
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||||
|
&addresses[3],
|
||||||
|
Amount::from_sat(10000),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
env.mine_blocks(1, None)?;
|
||||||
|
env.wait_until_electrum_sees_block()?;
|
||||||
|
|
||||||
|
// use a full checkpoint linked list (since this is not what we are testing)
|
||||||
|
let cp_tip = env.make_checkpoint_tip();
|
||||||
|
|
||||||
|
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||||
|
// will.
|
||||||
|
let full_scan_update = {
|
||||||
|
let request =
|
||||||
|
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||||
|
client.full_scan(request, 3, 1, false)?
|
||||||
|
};
|
||||||
|
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||||
|
assert!(full_scan_update.last_active_indices.is_empty());
|
||||||
|
let full_scan_update = {
|
||||||
|
let request =
|
||||||
|
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||||
|
client.full_scan(request, 4, 1, false)?
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
full_scan_update
|
||||||
|
.graph_update
|
||||||
|
.full_txs()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.txid,
|
||||||
|
txid_4th_addr
|
||||||
|
);
|
||||||
|
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||||
|
|
||||||
|
// Now receive a coin on the last address.
|
||||||
|
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||||
|
&addresses[addresses.len() - 1],
|
||||||
|
Amount::from_sat(10000),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(1),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
env.mine_blocks(1, None)?;
|
||||||
|
env.wait_until_electrum_sees_block()?;
|
||||||
|
|
||||||
|
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||||
|
// The last active indice won't be updated in the first case but will in the second one.
|
||||||
|
let full_scan_update = {
|
||||||
|
let request =
|
||||||
|
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||||
|
client.full_scan(request, 5, 1, false)?
|
||||||
|
};
|
||||||
|
let txs: HashSet<_> = full_scan_update
|
||||||
|
.graph_update
|
||||||
|
.full_txs()
|
||||||
|
.map(|tx| tx.txid)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(txs.len(), 1);
|
||||||
|
assert!(txs.contains(&txid_4th_addr));
|
||||||
|
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||||
|
let full_scan_update = {
|
||||||
|
let request =
|
||||||
|
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||||
|
client.full_scan(request, 6, 1, false)?
|
||||||
|
};
|
||||||
|
let txs: HashSet<_> = full_scan_update
|
||||||
|
.graph_update
|
||||||
|
.full_txs()
|
||||||
|
.map(|tx| tx.txid)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(txs.len(), 2);
|
||||||
|
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||||
|
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure that [`ElectrumExt`] can sync properly.
|
/// Ensure that [`ElectrumExt`] can sync properly.
|
||||||
///
|
///
|
||||||
/// 1. Mine 101 blocks.
|
/// 1. Mine 101 blocks.
|
||||||
@ -44,7 +262,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Setup receiver.
|
// Setup receiver.
|
||||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
|
||||||
let mut recv_index = SpkTxOutIndex::default();
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
recv_index.insert_spk((), spk_to_track.clone());
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
recv_index
|
recv_index
|
||||||
@ -61,14 +279,11 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Sync up to tip.
|
// Sync up to tip.
|
||||||
env.wait_until_electrum_sees_block()?;
|
env.wait_until_electrum_sees_block()?;
|
||||||
let update = client
|
let update = client.sync(
|
||||||
.sync(
|
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks(core::iter::once(spk_to_track)),
|
||||||
SyncRequest::from_chain_tip(recv_chain.tip())
|
|
||||||
.chain_spks(core::iter::once(spk_to_track)),
|
|
||||||
5,
|
5,
|
||||||
true,
|
true,
|
||||||
)?
|
)?;
|
||||||
.with_confirmation_time_height_anchor(&client)?;
|
|
||||||
|
|
||||||
let _ = recv_chain
|
let _ = recv_chain
|
||||||
.apply_update(update.chain_update)
|
.apply_update(update.chain_update)
|
||||||
@ -137,7 +352,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Setup receiver.
|
// Setup receiver.
|
||||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
|
||||||
let mut recv_index = SpkTxOutIndex::default();
|
let mut recv_index = SpkTxOutIndex::default();
|
||||||
recv_index.insert_spk((), spk_to_track.clone());
|
recv_index.insert_spk((), spk_to_track.clone());
|
||||||
recv_index
|
recv_index
|
||||||
@ -147,20 +362,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||||
|
|
||||||
// Create transactions that are tracked by our receiver.
|
// Create transactions that are tracked by our receiver.
|
||||||
|
let mut txids = vec![];
|
||||||
|
let mut hashes = vec![];
|
||||||
for _ in 0..REORG_COUNT {
|
for _ in 0..REORG_COUNT {
|
||||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
txids.push(env.send(&addr_to_track, SEND_AMOUNT)?);
|
||||||
env.mine_blocks(1, None)?;
|
hashes.extend(env.mine_blocks(1, None)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync up to tip.
|
// Sync up to tip.
|
||||||
env.wait_until_electrum_sees_block()?;
|
env.wait_until_electrum_sees_block()?;
|
||||||
let update = client
|
let update = client.sync(
|
||||||
.sync(
|
|
||||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||||
5,
|
5,
|
||||||
false,
|
false,
|
||||||
)?
|
)?;
|
||||||
.with_confirmation_time_height_anchor(&client)?;
|
|
||||||
|
|
||||||
let _ = recv_chain
|
let _ = recv_chain
|
||||||
.apply_update(update.chain_update)
|
.apply_update(update.chain_update)
|
||||||
@ -169,6 +384,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// Retain a snapshot of all anchors before reorg process.
|
// Retain a snapshot of all anchors before reorg process.
|
||||||
let initial_anchors = update.graph_update.all_anchors();
|
let initial_anchors = update.graph_update.all_anchors();
|
||||||
|
let anchors: Vec<_> = initial_anchors.iter().cloned().collect();
|
||||||
|
assert_eq!(anchors.len(), REORG_COUNT);
|
||||||
|
for i in 0..REORG_COUNT {
|
||||||
|
let (anchor, txid) = anchors[i];
|
||||||
|
assert_eq!(anchor.block_id.hash, hashes[i]);
|
||||||
|
assert_eq!(txid, txids[i]);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if initial balance is correct.
|
// Check if initial balance is correct.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -185,13 +407,11 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
env.reorg_empty_blocks(depth)?;
|
env.reorg_empty_blocks(depth)?;
|
||||||
|
|
||||||
env.wait_until_electrum_sees_block()?;
|
env.wait_until_electrum_sees_block()?;
|
||||||
let update = client
|
let update = client.sync(
|
||||||
.sync(
|
|
||||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||||
5,
|
5,
|
||||||
false,
|
false,
|
||||||
)?
|
)?;
|
||||||
.with_confirmation_time_height_anchor(&client)?;
|
|
||||||
|
|
||||||
let _ = recv_chain
|
let _ = recv_chain
|
||||||
.apply_update(update.chain_update)
|
.apply_update(update.chain_update)
|
||||||
|
@ -6,7 +6,7 @@ use bdk_chain::{
|
|||||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
local_chain::CheckPoint,
|
local_chain::CheckPoint,
|
||||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
BlockId, ConfirmationBlockTime, TxGraph,
|
||||||
};
|
};
|
||||||
use bdk_chain::{Anchor, Indexed};
|
use bdk_chain::{Anchor, Indexed};
|
||||||
use esplora_client::{Amount, TxStatus};
|
use esplora_client::{Amount, TxStatus};
|
||||||
@ -240,10 +240,10 @@ async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
|||||||
>,
|
>,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
|
||||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
@ -333,7 +333,7 @@ async fn sync_for_index_and_graph(
|
|||||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
|
||||||
let mut graph = full_scan_for_index_and_graph(
|
let mut graph = full_scan_for_index_and_graph(
|
||||||
client,
|
client,
|
||||||
[(
|
[(
|
||||||
|
@ -6,7 +6,7 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe
|
|||||||
use bdk_chain::{
|
use bdk_chain::{
|
||||||
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||||
local_chain::CheckPoint,
|
local_chain::CheckPoint,
|
||||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
BlockId, ConfirmationBlockTime, TxGraph,
|
||||||
};
|
};
|
||||||
use bdk_chain::{Anchor, Indexed};
|
use bdk_chain::{Anchor, Indexed};
|
||||||
use esplora_client::TxStatus;
|
use esplora_client::TxStatus;
|
||||||
@ -219,10 +219,10 @@ fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
|
|||||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = Indexed<ScriptBuf>>>,
|
keychain_spks: BTreeMap<K, impl IntoIterator<Item = Indexed<ScriptBuf>>>,
|
||||||
stop_gap: usize,
|
stop_gap: usize,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
|
||||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||||
let mut tx_graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||||
let mut last_active_indices = BTreeMap::<K, u32>::new();
|
let mut last_active_indices = BTreeMap::<K, u32>::new();
|
||||||
|
|
||||||
for (keychain, spks) in keychain_spks {
|
for (keychain, spks) in keychain_spks {
|
||||||
@ -315,7 +315,7 @@ fn sync_for_index_and_graph_blocking(
|
|||||||
txids: impl IntoIterator<Item = Txid>,
|
txids: impl IntoIterator<Item = Txid>,
|
||||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||||
parallel_requests: usize,
|
parallel_requests: usize,
|
||||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
|
||||||
let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking(
|
let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking(
|
||||||
client,
|
client,
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
|
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
|
||||||
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
|
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
|
||||||
|
|
||||||
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
use bdk_chain::{BlockId, ConfirmationBlockTime};
|
||||||
use esplora_client::TxStatus;
|
use esplora_client::TxStatus;
|
||||||
|
|
||||||
pub use esplora_client;
|
pub use esplora_client;
|
||||||
@ -31,7 +31,7 @@ mod async_ext;
|
|||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
pub use async_ext::*;
|
pub use async_ext::*;
|
||||||
|
|
||||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
|
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationBlockTime> {
|
||||||
if let TxStatus {
|
if let TxStatus {
|
||||||
block_height: Some(height),
|
block_height: Some(height),
|
||||||
block_hash: Some(hash),
|
block_hash: Some(hash),
|
||||||
@ -39,9 +39,8 @@ fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor>
|
|||||||
..
|
..
|
||||||
} = status.clone()
|
} = status.clone()
|
||||||
{
|
{
|
||||||
Some(ConfirmationTimeHeightAnchor {
|
Some(ConfirmationBlockTime {
|
||||||
anchor_block: BlockId { height, hash },
|
block_id: BlockId { height, hash },
|
||||||
confirmation_height: height,
|
|
||||||
confirmation_time: time,
|
confirmation_time: time,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -547,10 +547,7 @@ mod test {
|
|||||||
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
|
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
|
||||||
use bdk_chain::miniscript::Descriptor;
|
use bdk_chain::miniscript::Descriptor;
|
||||||
use bdk_chain::CombinedChangeSet;
|
use bdk_chain::CombinedChangeSet;
|
||||||
use bdk_chain::{
|
use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt};
|
||||||
indexed_tx_graph, tx_graph, BlockId, ConfirmationHeightAnchor,
|
|
||||||
ConfirmationTimeHeightAnchor, DescriptorExt,
|
|
||||||
};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -561,37 +558,15 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor() {
|
fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() {
|
||||||
let (test_changesets, agg_test_changesets) =
|
let (test_changesets, agg_test_changesets) =
|
||||||
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
|
create_test_changesets(&|height, time, hash| ConfirmationBlockTime {
|
||||||
confirmation_height: height,
|
|
||||||
confirmation_time: time,
|
confirmation_time: time,
|
||||||
anchor_block: (height, hash).into(),
|
block_id: (height, hash).into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||||
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
|
let mut store = Store::<Keychain, ConfirmationBlockTime>::new(conn)
|
||||||
.expect("create new memory db store");
|
|
||||||
|
|
||||||
test_changesets.iter().for_each(|changeset| {
|
|
||||||
store.write(changeset).expect("write changeset");
|
|
||||||
});
|
|
||||||
|
|
||||||
let agg_changeset = store.read().expect("aggregated changeset");
|
|
||||||
|
|
||||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() {
|
|
||||||
let (test_changesets, agg_test_changesets) =
|
|
||||||
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
|
|
||||||
confirmation_height: height,
|
|
||||||
anchor_block: (height, hash).into(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
|
||||||
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
|
|
||||||
.expect("create new memory db store");
|
.expect("create new memory db store");
|
||||||
|
|
||||||
test_changesets.iter().for_each(|changeset| {
|
test_changesets.iter().for_each(|changeset| {
|
||||||
|
@ -128,7 +128,7 @@ impl FullyNodedExport {
|
|||||||
let blockheight = if include_blockheight {
|
let blockheight = if include_blockheight {
|
||||||
wallet.transactions().next().map_or(0, |canonical_tx| {
|
wallet.transactions().next().map_or(0, |canonical_tx| {
|
||||||
match canonical_tx.chain_position {
|
match canonical_tx.chain_position {
|
||||||
bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
|
bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height,
|
||||||
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
|
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -214,7 +214,7 @@ mod test {
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use crate::std::string::ToString;
|
use crate::std::string::ToString;
|
||||||
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
use bdk_chain::{BlockId, ConfirmationBlockTime};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||||
|
|
||||||
@ -233,15 +233,20 @@ mod test {
|
|||||||
};
|
};
|
||||||
let txid = transaction.compute_txid();
|
let txid = transaction.compute_txid();
|
||||||
let block_id = BlockId {
|
let block_id = BlockId {
|
||||||
height: 5001,
|
height: 5000,
|
||||||
hash: BlockHash::all_zeros(),
|
hash: BlockHash::all_zeros(),
|
||||||
};
|
};
|
||||||
wallet.insert_checkpoint(block_id).unwrap();
|
wallet.insert_checkpoint(block_id).unwrap();
|
||||||
|
wallet
|
||||||
|
.insert_checkpoint(BlockId {
|
||||||
|
height: 5001,
|
||||||
|
hash: BlockHash::all_zeros(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
wallet.insert_tx(transaction);
|
wallet.insert_tx(transaction);
|
||||||
let anchor = ConfirmationTimeHeightAnchor {
|
let anchor = ConfirmationBlockTime {
|
||||||
confirmation_height: 5000,
|
|
||||||
confirmation_time: 0,
|
confirmation_time: 0,
|
||||||
anchor_block: block_id,
|
block_id,
|
||||||
};
|
};
|
||||||
let mut graph = TxGraph::default();
|
let mut graph = TxGraph::default();
|
||||||
let _ = graph.insert_anchor(txid, anchor);
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
|
@ -28,7 +28,7 @@ use bdk_chain::{
|
|||||||
},
|
},
|
||||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||||
tx_graph::{CanonicalTx, TxGraph, TxNode},
|
tx_graph::{CanonicalTx, TxGraph, TxNode},
|
||||||
BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, Indexed,
|
BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed,
|
||||||
IndexedTxGraph, Merge,
|
IndexedTxGraph, Merge,
|
||||||
};
|
};
|
||||||
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
|
||||||
@ -104,7 +104,7 @@ pub struct Wallet {
|
|||||||
signers: Arc<SignersContainer>,
|
signers: Arc<SignersContainer>,
|
||||||
change_signers: Arc<SignersContainer>,
|
change_signers: Arc<SignersContainer>,
|
||||||
chain: LocalChain,
|
chain: LocalChain,
|
||||||
indexed_graph: IndexedTxGraph<ConfirmationTimeHeightAnchor, KeychainTxOutIndex<KeychainKind>>,
|
indexed_graph: IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<KeychainKind>>,
|
||||||
stage: ChangeSet,
|
stage: ChangeSet,
|
||||||
network: Network,
|
network: Network,
|
||||||
secp: SecpCtx,
|
secp: SecpCtx,
|
||||||
@ -120,7 +120,7 @@ pub struct Update {
|
|||||||
pub last_active_indices: BTreeMap<KeychainKind, u32>,
|
pub last_active_indices: BTreeMap<KeychainKind, u32>,
|
||||||
|
|
||||||
/// Update for the wallet's internal [`TxGraph`].
|
/// Update for the wallet's internal [`TxGraph`].
|
||||||
pub graph: TxGraph<ConfirmationTimeHeightAnchor>,
|
pub graph: TxGraph<ConfirmationBlockTime>,
|
||||||
|
|
||||||
/// Update for the wallet's internal [`LocalChain`].
|
/// Update for the wallet's internal [`LocalChain`].
|
||||||
///
|
///
|
||||||
@ -149,7 +149,7 @@ impl From<SyncResult> for Update {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The changes made to a wallet by applying an [`Update`].
|
/// The changes made to a wallet by applying an [`Update`].
|
||||||
pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationTimeHeightAnchor>;
|
pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationBlockTime>;
|
||||||
|
|
||||||
/// A derived address and the index it was found at.
|
/// A derived address and the index it was found at.
|
||||||
/// For convenience this automatically derefs to `Address`
|
/// For convenience this automatically derefs to `Address`
|
||||||
@ -1007,7 +1007,7 @@ impl Wallet {
|
|||||||
/// match canonical_tx.chain_position {
|
/// match canonical_tx.chain_position {
|
||||||
/// ChainPosition::Confirmed(anchor) => println!(
|
/// ChainPosition::Confirmed(anchor) => println!(
|
||||||
/// "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
|
/// "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
|
||||||
/// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash,
|
/// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash,
|
||||||
/// ),
|
/// ),
|
||||||
/// ChainPosition::Unconfirmed(last_seen) => println!(
|
/// ChainPosition::Unconfirmed(last_seen) => println!(
|
||||||
/// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
|
/// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
|
||||||
@ -1020,7 +1020,7 @@ impl Wallet {
|
|||||||
pub fn get_tx(
|
pub fn get_tx(
|
||||||
&self,
|
&self,
|
||||||
txid: Txid,
|
txid: Txid,
|
||||||
) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
|
) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationBlockTime>> {
|
||||||
let graph = self.indexed_graph.graph();
|
let graph = self.indexed_graph.graph();
|
||||||
|
|
||||||
Some(CanonicalTx {
|
Some(CanonicalTx {
|
||||||
@ -1076,8 +1076,7 @@ impl Wallet {
|
|||||||
/// Iterate over the transactions in the wallet.
|
/// Iterate over the transactions in the wallet.
|
||||||
pub fn transactions(
|
pub fn transactions(
|
||||||
&self,
|
&self,
|
||||||
) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> + '_
|
) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationBlockTime>> + '_ {
|
||||||
{
|
|
||||||
self.indexed_graph
|
self.indexed_graph
|
||||||
.graph()
|
.graph()
|
||||||
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
|
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
|
||||||
@ -1807,7 +1806,7 @@ impl Wallet {
|
|||||||
.graph()
|
.graph()
|
||||||
.get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
|
.get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
|
||||||
.map(|chain_position| match chain_position {
|
.map(|chain_position| match chain_position {
|
||||||
ChainPosition::Confirmed(a) => a.confirmation_height,
|
ChainPosition::Confirmed(a) => a.block_id.height,
|
||||||
ChainPosition::Unconfirmed(_) => u32::MAX,
|
ChainPosition::Unconfirmed(_) => u32::MAX,
|
||||||
});
|
});
|
||||||
let current_height = sign_options
|
let current_height = sign_options
|
||||||
@ -2245,7 +2244,7 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the inner [`TxGraph`].
|
/// Get a reference to the inner [`TxGraph`].
|
||||||
pub fn tx_graph(&self) -> &TxGraph<ConfirmationTimeHeightAnchor> {
|
pub fn tx_graph(&self) -> &TxGraph<ConfirmationBlockTime> {
|
||||||
self.indexed_graph.graph()
|
self.indexed_graph.graph()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2253,7 +2252,7 @@ impl Wallet {
|
|||||||
/// because they haven't been broadcast.
|
/// because they haven't been broadcast.
|
||||||
pub fn unbroadcast_transactions(
|
pub fn unbroadcast_transactions(
|
||||||
&self,
|
&self,
|
||||||
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
|
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, ConfirmationBlockTime>> {
|
||||||
self.tx_graph().txs_with_no_anchor_or_last_seen()
|
self.tx_graph().txs_with_no_anchor_or_last_seen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2373,8 +2372,8 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet {
|
impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime>> for Wallet {
|
||||||
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor> {
|
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime> {
|
||||||
self.indexed_graph.graph()
|
self.indexed_graph.graph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2413,7 +2412,7 @@ where
|
|||||||
fn new_local_utxo(
|
fn new_local_utxo(
|
||||||
keychain: KeychainKind,
|
keychain: KeychainKind,
|
||||||
derivation_index: u32,
|
derivation_index: u32,
|
||||||
full_txo: FullTxOut<ConfirmationTimeHeightAnchor>,
|
full_txo: FullTxOut<ConfirmationBlockTime>,
|
||||||
) -> LocalOutput {
|
) -> LocalOutput {
|
||||||
LocalOutput {
|
LocalOutput {
|
||||||
outpoint: full_txo.outpoint,
|
outpoint: full_txo.outpoint,
|
||||||
@ -2476,7 +2475,7 @@ macro_rules! floating_rate {
|
|||||||
macro_rules! doctest_wallet {
|
macro_rules! doctest_wallet {
|
||||||
() => {{
|
() => {{
|
||||||
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
|
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
|
||||||
use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId, TxGraph};
|
use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph};
|
||||||
use $crate::wallet::{Update, Wallet};
|
use $crate::wallet::{Update, Wallet};
|
||||||
use $crate::KeychainKind;
|
use $crate::KeychainKind;
|
||||||
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
||||||
@ -2499,13 +2498,13 @@ macro_rules! doctest_wallet {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
let block = BlockId { height: 1_000, hash: BlockHash::all_zeros() };
|
let block_id = BlockId { height: 500, hash: BlockHash::all_zeros() };
|
||||||
let _ = wallet.insert_checkpoint(block);
|
let _ = wallet.insert_checkpoint(block_id);
|
||||||
|
let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
|
||||||
let _ = wallet.insert_tx(tx);
|
let _ = wallet.insert_tx(tx);
|
||||||
let anchor = ConfirmationTimeHeightAnchor {
|
let anchor = ConfirmationBlockTime {
|
||||||
confirmation_height: 500,
|
|
||||||
confirmation_time: 50_000,
|
confirmation_time: 50_000,
|
||||||
anchor_block: block,
|
block_id,
|
||||||
};
|
};
|
||||||
let mut graph = TxGraph::default();
|
let mut graph = TxGraph::default();
|
||||||
let _ = graph.insert_anchor(txid, anchor);
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor, TxGraph};
|
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
|
||||||
use bdk_wallet::{
|
use bdk_wallet::{
|
||||||
wallet::{Update, Wallet},
|
wallet::{Update, Wallet},
|
||||||
KeychainKind, LocalOutput,
|
KeychainKind, LocalOutput,
|
||||||
@ -65,6 +65,12 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.insert_checkpoint(BlockId {
|
||||||
|
height: 42,
|
||||||
|
hash: BlockHash::all_zeros(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
wallet
|
wallet
|
||||||
.insert_checkpoint(BlockId {
|
.insert_checkpoint(BlockId {
|
||||||
height: 1_000,
|
height: 1_000,
|
||||||
@ -205,9 +211,8 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir
|
|||||||
.local_chain()
|
.local_chain()
|
||||||
.range(height..)
|
.range(height..)
|
||||||
.last()
|
.last()
|
||||||
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
|
.map(|anchor_cp| ConfirmationBlockTime {
|
||||||
anchor_block: anchor_cp.block_id(),
|
block_id: anchor_cp.block_id(),
|
||||||
confirmation_height: height,
|
|
||||||
confirmation_time: time,
|
confirmation_time: time,
|
||||||
})
|
})
|
||||||
.expect("confirmation height cannot be greater than tip");
|
.expect("confirmation height cannot be greater than tip");
|
||||||
|
@ -16,7 +16,7 @@ use bdk_chain::{
|
|||||||
indexed_tx_graph,
|
indexed_tx_graph,
|
||||||
indexer::keychain_txout,
|
indexer::keychain_txout,
|
||||||
local_chain::{self, LocalChain},
|
local_chain::{self, LocalChain},
|
||||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, Merge,
|
ConfirmationBlockTime, IndexedTxGraph, Merge,
|
||||||
};
|
};
|
||||||
use example_cli::{
|
use example_cli::{
|
||||||
anyhow,
|
anyhow,
|
||||||
@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
|
|||||||
|
|
||||||
type ChangeSet = (
|
type ChangeSet = (
|
||||||
local_chain::ChangeSet,
|
local_chain::ChangeSet,
|
||||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain_txout::ChangeSet<Keychain>>,
|
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -10,7 +10,7 @@ use bdk_chain::{
|
|||||||
indexer::keychain_txout,
|
indexer::keychain_txout,
|
||||||
local_chain::{self, LocalChain},
|
local_chain::{self, LocalChain},
|
||||||
spk_client::{FullScanRequest, SyncRequest},
|
spk_client::{FullScanRequest, SyncRequest},
|
||||||
ConfirmationHeightAnchor, Merge,
|
ConfirmationBlockTime, Merge,
|
||||||
};
|
};
|
||||||
use bdk_electrum::{
|
use bdk_electrum::{
|
||||||
electrum_client::{self, Client, ElectrumApi},
|
electrum_client::{self, Client, ElectrumApi},
|
||||||
@ -100,7 +100,7 @@ pub struct ScanOptions {
|
|||||||
|
|
||||||
type ChangeSet = (
|
type ChangeSet = (
|
||||||
local_chain::ChangeSet,
|
local_chain::ChangeSet,
|
||||||
indexed_tx_graph::ChangeSet<ConfirmationHeightAnchor, keychain_txout::ChangeSet<Keychain>>,
|
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
@ -193,8 +193,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let res = client
|
let res = client
|
||||||
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
|
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
|
||||||
.context("scanning the blockchain")?
|
.context("scanning the blockchain")?;
|
||||||
.with_confirmation_height_anchor();
|
|
||||||
(
|
(
|
||||||
res.chain_update,
|
res.chain_update,
|
||||||
res.graph_update,
|
res.graph_update,
|
||||||
@ -317,8 +316,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let res = client
|
let res = client
|
||||||
.sync(request, scan_options.batch_size, false)
|
.sync(request, scan_options.batch_size, false)
|
||||||
.context("scanning the blockchain")?
|
.context("scanning the blockchain")?;
|
||||||
.with_confirmation_height_anchor();
|
|
||||||
|
|
||||||
// drop lock on graph and chain
|
// drop lock on graph and chain
|
||||||
drop((graph, chain));
|
drop((graph, chain));
|
||||||
@ -340,7 +338,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let chain_changeset = chain.apply_update(chain_update)?;
|
let chain_changeset = chain.apply_update(chain_update)?;
|
||||||
|
|
||||||
let mut indexed_tx_graph_changeset =
|
let mut indexed_tx_graph_changeset =
|
||||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
indexed_tx_graph::ChangeSet::<ConfirmationBlockTime, _>::default();
|
||||||
if let Some(keychain_update) = keychain_update {
|
if let Some(keychain_update) = keychain_update {
|
||||||
let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
|
let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
|
||||||
indexed_tx_graph_changeset.merge(keychain_changeset.into());
|
indexed_tx_graph_changeset.merge(keychain_changeset.into());
|
||||||
|
@ -10,7 +10,7 @@ use bdk_chain::{
|
|||||||
indexer::keychain_txout,
|
indexer::keychain_txout,
|
||||||
local_chain::{self, LocalChain},
|
local_chain::{self, LocalChain},
|
||||||
spk_client::{FullScanRequest, SyncRequest},
|
spk_client::{FullScanRequest, SyncRequest},
|
||||||
ConfirmationTimeHeightAnchor, Merge,
|
ConfirmationBlockTime, Merge,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bdk_esplora::{esplora_client, EsploraExt};
|
use bdk_esplora::{esplora_client, EsploraExt};
|
||||||
@ -26,7 +26,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db";
|
|||||||
|
|
||||||
type ChangeSet = (
|
type ChangeSet = (
|
||||||
local_chain::ChangeSet,
|
local_chain::ChangeSet,
|
||||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain_txout::ChangeSet<Keychain>>,
|
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet<Keychain>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
|
@ -63,9 +63,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
})
|
})
|
||||||
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
|
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
|
||||||
|
|
||||||
let mut update = client
|
let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?;
|
||||||
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
|
|
||||||
.with_confirmation_time_height_anchor(&client)?;
|
|
||||||
|
|
||||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user