Merge bitcoindevkit/bdk#1172: Introduce block-by-block API to bdk::Wallet and add RPC wallet example

a4f28c079e chore: improve LocalChain::apply_header_connected_to doc (LLFourn)
8ec65f0b8e feat(example): add RPC wallet example (Vladimir Fomene)
a7d01dc39a feat(chain)!: make `IndexedTxGraph::apply_block_relevant` more efficient (志宇)
e0512acf94 feat(bitcoind_rpc)!: emissions include checkpoint and connected_to data (志宇)
8f2d4d9d40 test(chain): `LocalChain` test for update that is shorter than original (志宇)
9467cad55d feat(wallet): introduce block-by-block api (Vladimir Fomene)
d3e5095df1 feat(chain): add `apply_header..` methods to `LocalChain` (志宇)
2b61a122ff feat(chain): add `CheckPoint::from_block_ids` convenience method (志宇)

Pull request description:

  ### Description

  Introduce block-by-block API for `bdk::Wallet`. A `wallet_rpc` example is added to demonstrate syncing `bdk::Wallet` with the `bdk_bitcoind_rpc` chain-source crate.

  The API of `bdk_bitcoind_rpc::Emitter` is changed so the receiver knows how to connect to the block emitted.

  ### Notes to the reviewers

  ### Changelog notice

  Added
  * `Wallet` methods to apply full blocks (`apply_block` and `apply_block_connected_to`) and a method to apply a batch of unconfirmed transactions (`apply_unconfirmed_txs`).
  * `CheckPoint::from_block_ids` convenience method.
  * `LocalChain` methods to apply a block header (`apply_header` and `apply_header_connected_to`).
  * Test to show that `LocalChain` can apply updates that are shorter than original. This will happen during reorgs if we sync wallet with `bdk_bitcoind_rpc::Emitter`.

  Fixed
  * `InsertTxError` now implements `std::error::Error`.

  #### 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:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

ACKs for top commit:
  LLFourn:
    self-ACK: a4f28c079e
  evanlinjin:
    ACK a4f28c079e

Tree-SHA512: e39fb65b4e69c0a6748d64eab12913dc9cfe5eb8355ab8fb68f60a37c3bb2e1489ddd8f2f138c6470135344f40e3dc671928f65d303fd41fb63f577b30895b60
This commit is contained in:
志宇
2024-01-19 23:07:20 +08:00
12 changed files with 983 additions and 89 deletions

View File

@@ -23,7 +23,9 @@ pub use bdk_chain::keychain::Balance;
use bdk_chain::{
indexed_tx_graph,
keychain::{self, KeychainTxOutIndex},
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
local_chain::{
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
},
tx_graph::{CanonicalTx, TxGraph},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
IndexedTxGraph, Persist, PersistBackend,
@@ -31,8 +33,8 @@ use bdk_chain::{
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{
absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
Weight, Witness,
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
Txid, Weight, Witness,
};
use bitcoin::{consensus::encode::serialize, BlockHash};
use bitcoin::{constants::genesis_block, psbt};
@@ -438,6 +440,55 @@ pub enum InsertTxError {
},
}
impl fmt::Display for InsertTxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height,
tx_height,
} => {
write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for InsertTxError {}
/// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)]
pub enum ApplyBlockError {
/// Occurs when the update chain cannot connect with original chain.
CannotConnect(CannotConnectError),
/// Occurs when the `connected_to` hash does not match the hash derived from `block`.
UnexpectedConnectedToHash {
/// Block hash of `connected_to`.
connected_to_hash: BlockHash,
/// Expected block hash of `connected_to`, as derived from `block`.
expected_hash: BlockHash,
},
}
impl fmt::Display for ApplyBlockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApplyBlockError::CannotConnect(err) => err.fmt(f),
ApplyBlockError::UnexpectedConnectedToHash {
expected_hash: block_hash,
connected_to_hash: checkpoint_hash,
} => write!(
f,
"`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
checkpoint_hash, block_hash
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ApplyBlockError {}
impl<D> Wallet<D> {
/// Initialize an empty [`Wallet`].
pub fn new<E: IntoWalletDescriptor>(
@@ -2329,7 +2380,7 @@ impl<D> Wallet<D> {
self.persist.commit().map(|c| c.is_some())
}
/// Returns the changes that will be staged with the next call to [`commit`].
/// Returns the changes that will be committed with the next call to [`commit`].
///
/// [`commit`]: Self::commit
pub fn staged(&self) -> &ChangeSet
@@ -2353,6 +2404,86 @@ impl<D> Wallet<D> {
pub fn local_chain(&self) -> &LocalChain {
&self.chain
}
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
/// `prev_blockhash` of the block's header.
///
/// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
/// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
///
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError>
where
D: PersistBackend<ChangeSet>,
{
let connected_to = match height.checked_sub(1) {
Some(prev_height) => BlockId {
height: prev_height,
hash: block.header.prev_blockhash,
},
None => BlockId {
height,
hash: block.block_hash(),
},
};
self.apply_block_connected_to(block, height, connected_to)
.map_err(|err| match err {
ApplyHeaderError::InconsistentBlocks => {
unreachable!("connected_to is derived from the block so must be consistent")
}
ApplyHeaderError::CannotConnect(err) => err,
})
}
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
/// block to the internal chain.
///
/// The `connected_to` parameter informs the wallet how this block connects to the internal
/// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
/// internal [`TxGraph`].
pub fn apply_block_connected_to(
&mut self,
block: &Block,
height: u32,
connected_to: BlockId,
) -> Result<(), ApplyHeaderError>
where
D: PersistBackend<ChangeSet>,
{
let mut changeset = ChangeSet::default();
changeset.append(
self.chain
.apply_header_connected_to(&block.header, height, connected_to)?
.into(),
);
changeset.append(
self.indexed_graph
.apply_block_relevant(block, height)
.into(),
);
self.persist.stage(changeset);
Ok(())
}
/// Apply relevant unconfirmed transactions to the wallet.
///
/// Transactions that are not relevant are filtered out.
///
/// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
/// when the transaction was last seen in the mempool. This is used for conflict resolution
/// when there is conflicting unconfirmed transactions. The transaction with the later
/// `last_seen` is prioritied.
pub fn apply_unconfirmed_txs<'t>(
&mut self,
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
) where
D: PersistBackend<ChangeSet>,
{
let indexed_graph_changeset = self
.indexed_graph
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
self.persist.stage(ChangeSet::from(indexed_graph_changeset));
}
}
impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet<D> {