Skip to content

Commit 25653d7

Browse files
committed
Merge #1172: Introduce block-by-block API to bdk::Wallet and add RPC wallet example
a4f28c0 chore: improve LocalChain::apply_header_connected_to doc (LLFourn) 8ec65f0 feat(example): add RPC wallet example (Vladimir Fomene) a7d01dc feat(chain)!: make `IndexedTxGraph::apply_block_relevant` more efficient (志宇) e0512ac feat(bitcoind_rpc)!: emissions include checkpoint and connected_to data (志宇) 8f2d4d9 test(chain): `LocalChain` test for update that is shorter than original (志宇) 9467cad feat(wallet): introduce block-by-block api (Vladimir Fomene) d3e5095 feat(chain): add `apply_header..` methods to `LocalChain` (志宇) 2b61a12 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: a4f28c0 evanlinjin: ACK a4f28c0 Tree-SHA512: e39fb65b4e69c0a6748d64eab12913dc9cfe5eb8355ab8fb68f60a37c3bb2e1489ddd8f2f138c6470135344f40e3dc671928f65d303fd41fb63f577b30895b60
2 parents 0a2a570 + a4f28c0 commit 25653d7

File tree

12 files changed

+983
-89
lines changed

12 files changed

+983
-89
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ members = [
1515
"example-crates/wallet_electrum",
1616
"example-crates/wallet_esplora_blocking",
1717
"example-crates/wallet_esplora_async",
18+
"example-crates/wallet_rpc",
1819
"nursery/tmp_plan",
1920
"nursery/coin_select"
2021
]

crates/bdk/src/wallet/mod.rs

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@ pub use bdk_chain::keychain::Balance;
2323
use bdk_chain::{
2424
indexed_tx_graph,
2525
keychain::{self, KeychainTxOutIndex},
26-
local_chain::{self, CannotConnectError, CheckPoint, CheckPointIter, LocalChain},
26+
local_chain::{
27+
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
28+
},
2729
tx_graph::{CanonicalTx, TxGraph},
2830
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
2931
IndexedTxGraph, Persist, PersistBackend,
3032
};
3133
use bitcoin::secp256k1::{All, Secp256k1};
3234
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
3335
use bitcoin::{
34-
absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
35-
Weight, Witness,
36+
absolute, Address, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut,
37+
Txid, Weight, Witness,
3638
};
3739
use bitcoin::{consensus::encode::serialize, BlockHash};
3840
use bitcoin::{constants::genesis_block, psbt};
@@ -438,6 +440,55 @@ pub enum InsertTxError {
438440
},
439441
}
440442

443+
impl fmt::Display for InsertTxError {
444+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445+
match self {
446+
InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
447+
tip_height,
448+
tx_height,
449+
} => {
450+
write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
451+
}
452+
}
453+
}
454+
}
455+
456+
#[cfg(feature = "std")]
457+
impl std::error::Error for InsertTxError {}
458+
459+
/// An error that may occur when applying a block to [`Wallet`].
460+
#[derive(Debug)]
461+
pub enum ApplyBlockError {
462+
/// Occurs when the update chain cannot connect with original chain.
463+
CannotConnect(CannotConnectError),
464+
/// Occurs when the `connected_to` hash does not match the hash derived from `block`.
465+
UnexpectedConnectedToHash {
466+
/// Block hash of `connected_to`.
467+
connected_to_hash: BlockHash,
468+
/// Expected block hash of `connected_to`, as derived from `block`.
469+
expected_hash: BlockHash,
470+
},
471+
}
472+
473+
impl fmt::Display for ApplyBlockError {
474+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475+
match self {
476+
ApplyBlockError::CannotConnect(err) => err.fmt(f),
477+
ApplyBlockError::UnexpectedConnectedToHash {
478+
expected_hash: block_hash,
479+
connected_to_hash: checkpoint_hash,
480+
} => write!(
481+
f,
482+
"`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)",
483+
checkpoint_hash, block_hash
484+
),
485+
}
486+
}
487+
}
488+
489+
#[cfg(feature = "std")]
490+
impl std::error::Error for ApplyBlockError {}
491+
441492
impl<D> Wallet<D> {
442493
/// Initialize an empty [`Wallet`].
443494
pub fn new<E: IntoWalletDescriptor>(
@@ -2329,7 +2380,7 @@ impl<D> Wallet<D> {
23292380
self.persist.commit().map(|c| c.is_some())
23302381
}
23312382

2332-
/// Returns the changes that will be staged with the next call to [`commit`].
2383+
/// Returns the changes that will be committed with the next call to [`commit`].
23332384
///
23342385
/// [`commit`]: Self::commit
23352386
pub fn staged(&self) -> &ChangeSet
@@ -2353,6 +2404,86 @@ impl<D> Wallet<D> {
23532404
pub fn local_chain(&self) -> &LocalChain {
23542405
&self.chain
23552406
}
2407+
2408+
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
2409+
/// `prev_blockhash` of the block's header.
2410+
///
2411+
/// This is a convenience method that is equivalent to calling [`apply_block_connected_to`]
2412+
/// with `prev_blockhash` and `height-1` as the `connected_to` parameter.
2413+
///
2414+
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
2415+
pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError>
2416+
where
2417+
D: PersistBackend<ChangeSet>,
2418+
{
2419+
let connected_to = match height.checked_sub(1) {
2420+
Some(prev_height) => BlockId {
2421+
height: prev_height,
2422+
hash: block.header.prev_blockhash,
2423+
},
2424+
None => BlockId {
2425+
height,
2426+
hash: block.block_hash(),
2427+
},
2428+
};
2429+
self.apply_block_connected_to(block, height, connected_to)
2430+
.map_err(|err| match err {
2431+
ApplyHeaderError::InconsistentBlocks => {
2432+
unreachable!("connected_to is derived from the block so must be consistent")
2433+
}
2434+
ApplyHeaderError::CannotConnect(err) => err,
2435+
})
2436+
}
2437+
2438+
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
2439+
/// block to the internal chain.
2440+
///
2441+
/// The `connected_to` parameter informs the wallet how this block connects to the internal
2442+
/// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the
2443+
/// internal [`TxGraph`].
2444+
pub fn apply_block_connected_to(
2445+
&mut self,
2446+
block: &Block,
2447+
height: u32,
2448+
connected_to: BlockId,
2449+
) -> Result<(), ApplyHeaderError>
2450+
where
2451+
D: PersistBackend<ChangeSet>,
2452+
{
2453+
let mut changeset = ChangeSet::default();
2454+
changeset.append(
2455+
self.chain
2456+
.apply_header_connected_to(&block.header, height, connected_to)?
2457+
.into(),
2458+
);
2459+
changeset.append(
2460+
self.indexed_graph
2461+
.apply_block_relevant(block, height)
2462+
.into(),
2463+
);
2464+
self.persist.stage(changeset);
2465+
Ok(())
2466+
}
2467+
2468+
/// Apply relevant unconfirmed transactions to the wallet.
2469+
///
2470+
/// Transactions that are not relevant are filtered out.
2471+
///
2472+
/// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
2473+
/// when the transaction was last seen in the mempool. This is used for conflict resolution
2474+
/// when there is conflicting unconfirmed transactions. The transaction with the later
2475+
/// `last_seen` is prioritied.
2476+
pub fn apply_unconfirmed_txs<'t>(
2477+
&mut self,
2478+
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
2479+
) where
2480+
D: PersistBackend<ChangeSet>,
2481+
{
2482+
let indexed_graph_changeset = self
2483+
.indexed_graph
2484+
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
2485+
self.persist.stage(ChangeSet::from(indexed_graph_changeset));
2486+
}
23562487
}
23572488

23582489
impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet<D> {

crates/bitcoind_rpc/src/lib.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ pub struct Emitter<'c, C> {
4343
}
4444

4545
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
46-
/// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` and `start_height`.
46+
/// Construct a new [`Emitter`].
4747
///
48-
/// * `last_cp` is the check point used to find the latest block which is still part of the best
49-
/// chain.
50-
/// * `start_height` is the block height to start emitting blocks from.
48+
/// `last_cp` informs the emitter of the chain we are starting off with. This way, the emitter
49+
/// can start emission from a block that connects to the original chain.
50+
///
51+
/// `start_height` starts emission from a given height (if there are no conflicts with the
52+
/// original chain).
5153
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
5254
Self {
5355
client,
@@ -127,13 +129,58 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
127129
}
128130

129131
/// Emit the next block height and header (if any).
130-
pub fn next_header(&mut self) -> Result<Option<(u32, Header)>, bitcoincore_rpc::Error> {
131-
poll(self, |hash| self.client.get_block_header(hash))
132+
pub fn next_header(&mut self) -> Result<Option<BlockEvent<Header>>, bitcoincore_rpc::Error> {
133+
Ok(poll(self, |hash| self.client.get_block_header(hash))?
134+
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
132135
}
133136

134137
/// Emit the next block height and block (if any).
135-
pub fn next_block(&mut self) -> Result<Option<(u32, Block)>, bitcoincore_rpc::Error> {
136-
poll(self, |hash| self.client.get_block(hash))
138+
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
139+
Ok(poll(self, |hash| self.client.get_block(hash))?
140+
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
141+
}
142+
}
143+
144+
/// A newly emitted block from [`Emitter`].
145+
#[derive(Debug)]
146+
pub struct BlockEvent<B> {
147+
/// Either a full [`Block`] or [`Header`] of the new block.
148+
pub block: B,
149+
150+
/// The checkpoint of the new block.
151+
///
152+
/// A [`CheckPoint`] is a node of a linked list of [`BlockId`]s. This checkpoint is linked to
153+
/// all [`BlockId`]s originally passed in [`Emitter::new`] as well as emitted blocks since then.
154+
/// These blocks are guaranteed to be of the same chain.
155+
///
156+
/// This is important as BDK structures require block-to-apply to be connected with another
157+
/// block in the original chain.
158+
pub checkpoint: CheckPoint,
159+
}
160+
161+
impl<B> BlockEvent<B> {
162+
/// The block height of this new block.
163+
pub fn block_height(&self) -> u32 {
164+
self.checkpoint.height()
165+
}
166+
167+
/// The block hash of this new block.
168+
pub fn block_hash(&self) -> BlockHash {
169+
self.checkpoint.hash()
170+
}
171+
172+
/// The [`BlockId`] of a previous block that this block connects to.
173+
///
174+
/// This either returns a [`BlockId`] of a previously emitted block or from the chain we started
175+
/// with (passed in as `last_cp` in [`Emitter::new`]).
176+
///
177+
/// This value is derived from [`BlockEvent::checkpoint`].
178+
pub fn connected_to(&self) -> BlockId {
179+
match self.checkpoint.prev() {
180+
Some(prev_cp) => prev_cp.block_id(),
181+
// there is no previous checkpoint, so just connect with itself
182+
None => self.checkpoint.block_id(),
183+
}
137184
}
138185
}
139186

@@ -203,7 +250,7 @@ where
203250
fn poll<C, V, F>(
204251
emitter: &mut Emitter<C>,
205252
get_item: F,
206-
) -> Result<Option<(u32, V)>, bitcoincore_rpc::Error>
253+
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
207254
where
208255
C: bitcoincore_rpc::RpcApi,
209256
F: Fn(&BlockHash) -> Result<V, bitcoincore_rpc::Error>,
@@ -215,13 +262,14 @@ where
215262
let hash = res.hash;
216263
let item = get_item(&hash)?;
217264

218-
emitter.last_cp = emitter
265+
let new_cp = emitter
219266
.last_cp
220267
.clone()
221268
.push(BlockId { height, hash })
222269
.expect("must push");
270+
emitter.last_cp = new_cp.clone();
223271
emitter.last_block = Some(res);
224-
return Ok(Some((height, item)));
272+
return Ok(Some((new_cp, item)));
225273
}
226274
PollResponse::NoMoreBlocks => {
227275
emitter.last_block = None;

0 commit comments

Comments
 (0)