Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement linked-list LocalChain and add rpc-chain module/example #1002

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a93109
[local_chain] Implement `LocalChain` with linked list
evanlinjin Jun 3, 2023
adf8f8f
[bitcoind_rpc] Initial work on `BitcoindRpcIter`
evanlinjin Jun 19, 2023
8f2cf9f
[bitcoind_rpc] Introduce `prune_and_apply_update` for `IndexedTxGraph`
evanlinjin Jun 19, 2023
e34a6e0
[bitcoind_rpc] Initial work on RPC example
evanlinjin Jun 19, 2023
1c39e6c
[local_chain] API simplifications
evanlinjin Jun 20, 2023
6be2541
[local_chain] Fix incorrect optimisation logic in `update()`
evanlinjin Jun 23, 2023
01cc782
[esplora/electrum] Make updates more consistent
evanlinjin Jun 24, 2023
dd9f60c
Add more documentation to `LocalChain`
evanlinjin Jun 28, 2023
8918614
Add test for `IndexedTxGraph::prune_and_apply_update`
evanlinjin Jun 28, 2023
f7d499a
[bitcoind_rpc] Add docs.
evanlinjin Jun 29, 2023
8cab2c5
`bdk_esplora` redesign
evanlinjin Jul 3, 2023
861c27b
Attempt to make `LocalChain::update` independent of checkpoint map
evanlinjin Jul 5, 2023
5924b29
Implement simpler chain update algorithm
LLFourn Jul 7, 2023
4c95c83
Rethink local chain internals and API
LLFourn Jul 7, 2023
53c129b
Early exit when there are no update blocks left
LLFourn Jul 7, 2023
21e9f83
impl IntoIterator for CheckPoint
LLFourn Jul 8, 2023
2efa891
Add lb_height parameter when merging chains
evanlinjin Jul 9, 2023
7f4f3db
Add esplora CLI example
remix7531 Jun 5, 2023
fedb2ce
Use `introduce_older_blocks` bool variable for `LocalChain::update`
evanlinjin Jul 10, 2023
37f7494
Remove `prune` param in `Wallet::apply_update` and use 2 methods instead
evanlinjin Jul 10, 2023
02ef360
Rename `CheckPoint::extend` to `extend_with_blocks`
evanlinjin Jul 10, 2023
69b5e63
Update .gitignore to ignore example db files and remove dup. crate
evanlinjin Jul 18, 2023
61e47a2
Simplify `bdk_bitcoind_rpc` API
evanlinjin Jul 18, 2023
25a9879
[local_chain] API improvements and simplifications
evanlinjin Jul 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ Cargo.lock

*.swp
.idea

# Example persisted files.
*.db
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ members = [
"crates/file_store",
"crates/electrum",
"crates/esplora",
"crates/bitcoind_rpc",
"example-crates/example_cli",
"example-crates/example_electrum",
"example-crates/example_esplora",
"example-crates/example_rpc",
"example-crates/wallet_electrum",
"example-crates/wallet_esplora",
"example-crates/wallet_esplora_async",
Expand Down
2 changes: 1 addition & 1 deletion crates/bdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dev-getrandom-wasm = ["getrandom/js"]
lazy_static = "1.4"
env_logger = "0.7"
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
base64 = "^0.13"
base64 = "^0.21"
assert_matches = "1.5.0"

[package.metadata.docs.rs]
Expand Down
90 changes: 60 additions & 30 deletions crates/bdk/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use bdk_chain::keychain::Balance;
use bdk_chain::{
indexed_tx_graph::IndexedAdditions,
keychain::{KeychainTxOutIndex, LocalChangeSet, LocalUpdate},
local_chain::{self, LocalChain, UpdateNotConnectedError},
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
tx_graph::{CanonicalTx, TxGraph},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut,
IndexedTxGraph, Persist, PersistBackend,
Expand All @@ -32,8 +32,8 @@ use bitcoin::consensus::encode::serialize;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::psbt;
use bitcoin::{
Address, BlockHash, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script,
Sequence, Transaction, TxOut, Txid, Witness,
Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
Transaction, TxOut, Txid, Witness,
};
use core::fmt;
use core::ops::Deref;
Expand Down Expand Up @@ -245,7 +245,7 @@ impl<D> Wallet<D> {
};

let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
chain.apply_changeset(changeset.chain_changeset);
chain.apply_changeset(&changeset.chain_changeset);
indexed_graph.apply_additions(changeset.indexed_additions);

let persist = Persist::new(db);
Expand Down Expand Up @@ -370,19 +370,19 @@ impl<D> Wallet<D> {
.graph()
.filter_chain_unspents(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
self.indexed_graph.index.outpoints().iter().cloned(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
}

/// Get all the checkpoints the wallet is currently storing indexed by height.
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
self.chain.blocks()
pub fn checkpoints(&self) -> CheckPointIter {
self.chain.iter_checkpoints()
}

/// Returns the latest checkpoint.
pub fn latest_checkpoint(&self) -> Option<BlockId> {
pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
self.chain.tip()
}

Expand Down Expand Up @@ -420,7 +420,7 @@ impl<D> Wallet<D> {
.graph()
.filter_chain_unspents(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
core::iter::once((spk_i, op)),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
Expand All @@ -437,7 +437,7 @@ impl<D> Wallet<D> {
let canonical_tx = CanonicalTx {
observed_as: graph.get_chain_position(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
txid,
)?,
node: graph.get_tx_node(txid)?,
Expand All @@ -460,7 +460,7 @@ impl<D> Wallet<D> {
pub fn insert_checkpoint(
&mut self,
block_id: BlockId,
) -> Result<bool, local_chain::InsertBlockNotMatchingError>
) -> Result<bool, local_chain::InsertBlockError>
where
D: PersistBackend<ChangeSet>,
{
Expand Down Expand Up @@ -500,17 +500,17 @@ impl<D> Wallet<D> {
// anchor tx to checkpoint with lowest height that is >= position's height
let anchor = self
.chain
.blocks()
.heights()
.range(height..)
.next()
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height: self.chain.tip().map(|b| b.height),
tip_height: self.chain.tip().map(|b| b.height()),
tx_height: height,
})
.map(|(&anchor_height, &anchor_hash)| ConfirmationTimeAnchor {
.map(|(&anchor_height, &hash)| ConfirmationTimeAnchor {
anchor_block: BlockId {
height: anchor_height,
hash: anchor_hash,
hash,
},
confirmation_height: height,
confirmation_time: time,
Expand All @@ -531,17 +531,18 @@ impl<D> Wallet<D> {
pub fn transactions(
&self,
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> + '_ {
self.indexed_graph
.graph()
.list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
self.indexed_graph.graph().list_chain_txs(
&self.chain,
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
)
}

/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
/// values.
pub fn get_balance(&self) -> Balance {
self.indexed_graph.graph().balance(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
self.indexed_graph.index.outpoints().iter().cloned(),
|&(k, _), _| k == KeychainKind::Internal,
)
Expand Down Expand Up @@ -715,8 +716,7 @@ impl<D> Wallet<D> {
None => self
.chain
.tip()
.and_then(|cp| cp.height.into())
.map(|height| LockTime::from_height(height).expect("Invalid height")),
.map(|cp| LockTime::from_height(cp.height()).expect("Invalid height")),
h => h,
};

Expand Down Expand Up @@ -1030,7 +1030,7 @@ impl<D> Wallet<D> {
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
let graph = self.indexed_graph.graph();
let txout_index = &self.indexed_graph.index;
let chain_tip = self.chain.tip().unwrap_or_default();
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();

let mut tx = graph
.get_tx(txid)
Expand Down Expand Up @@ -1265,7 +1265,7 @@ impl<D> Wallet<D> {
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions,
) -> Result<bool, Error> {
let chain_tip = self.chain.tip().unwrap_or_default();
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();

let tx = &psbt.unsigned_tx;
let mut finished = true;
Expand All @@ -1288,7 +1288,7 @@ impl<D> Wallet<D> {
});
let current_height = sign_options
.assume_height
.or(self.chain.tip().map(|b| b.height));
.or(self.chain.tip().map(|b| b.height()));

debug!(
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
Expand Down Expand Up @@ -1433,7 +1433,7 @@ impl<D> Wallet<D> {
must_only_use_confirmed_tx: bool,
current_height: Option<u32>,
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
let chain_tip = self.chain.tip().unwrap_or_default();
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
// must_spend <- manually selected utxos
// may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos();
Expand Down Expand Up @@ -1697,24 +1697,54 @@ impl<D> Wallet<D> {
}

/// Applies an update to the wallet and stages the changes (but does not [`commit`] them).
///
/// This returns whether the `update` resulted in any changes.
/// Returns whether the `update` resulted in any changes.
///
/// Usually you create an `update` by interacting with some blockchain data source and inserting
/// transactions related to your wallet into it.
///
/// [`commit`]: Self::commit
pub fn apply_update(&mut self, update: Update) -> Result<bool, UpdateNotConnectedError>
pub fn apply_update(&mut self, update: Update) -> Result<bool, CannotConnectError>
where
D: PersistBackend<ChangeSet>,
{
let mut changeset = ChangeSet::from(self.chain.apply_update(update.chain)?);
let (_, index_additions) = self
.indexed_graph
.index
.reveal_to_target_multi(&update.keychain);
changeset.append(ChangeSet::from(IndexedAdditions::from(index_additions)));
changeset.append(ChangeSet::from(
self.indexed_graph.apply_update(update.graph),
));

let changed = !changeset.is_empty();
self.persist.stage(changeset);
Ok(changed)
}

/// Applies and update to the wallet (after pruning it of irrelevant transactions) and stages
/// the changes (does not [`commit`] them).
///
/// Irrelevant transactions are transactions that do not change the UTXO set of tracked script
/// pubkeys (script pubkeys that are derived from tracked descriptors).
///
/// To apply an update without pruning, use [`apply_update`].
///
/// [`commit`]: Self::commit
/// [`apply_update`]: Self::apply_update
pub fn prune_and_apply_update(&mut self, update: Update) -> Result<bool, CannotConnectError>
where
D: PersistBackend<ChangeSet>,
{
let mut changeset: ChangeSet = self.chain.apply_update(update.chain)?.into();
let mut changeset = ChangeSet::from(self.chain.apply_update(update.chain)?);
let (_, index_additions) = self
.indexed_graph
.index
.reveal_to_target_multi(&update.keychain);
changeset.append(ChangeSet::from(IndexedAdditions::from(index_additions)));
changeset.append(self.indexed_graph.apply_update(update.graph).into());
changeset.append(ChangeSet::from(
self.indexed_graph.prune_and_apply_update(update.graph),
));

let changed = !changeset.is_empty();
self.persist.stage(changeset);
Expand Down
15 changes: 7 additions & 8 deletions crates/bdk/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->

fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
let height = match wallet.latest_checkpoint() {
Some(BlockId { height, .. }) => ConfirmationTime::Confirmed { height, time: 0 },
Some(cp) => ConfirmationTime::Confirmed {
height: cp.height(),
time: 0,
},
None => ConfirmationTime::Unconfirmed { last_seen: 0 },
};
receive_output(wallet, value, height)
Expand Down Expand Up @@ -222,7 +225,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
// If there's no current_height we're left with using the last sync height
assert_eq!(
psbt.unsigned_tx.lock_time.0,
wallet.latest_checkpoint().unwrap().height
wallet.latest_checkpoint().unwrap().height()
);
}

Expand Down Expand Up @@ -426,11 +429,7 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
fn test_create_tx_drain_to_and_utxos() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New);
let utxos: Vec<_> = wallet
.list_unspent()
.into_iter()
.map(|u| u.outpoint)
.collect();
let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
Expand Down Expand Up @@ -1482,7 +1481,7 @@ fn test_bump_fee_drain_wallet() {
.insert_tx(
tx.clone(),
ConfirmationTime::Confirmed {
height: wallet.latest_checkpoint().unwrap().height,
height: wallet.latest_checkpoint().unwrap().height(),
time: 42_000,
},
)
Expand Down
11 changes: 11 additions & 0 deletions crates/bitcoind_rpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "bdk_bitcoind_rpc"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bdk_chain = { path = "../chain", version = "0.5.0", features = ["serde", "miniscript"] }
bitcoincore-rpc = { version = "0.16" }
anyhow = { version = "1" }
Loading