Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
6 changes: 2 additions & 4 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,10 @@ jobs:
with:
toolchain: ${{ needs.prepare.outputs.rust_version }}
cache: true
- name: Build (no default features + download)
run: cargo build --no-default-features --features download
- name: Build (default features)
run: cargo build
- name: Build (no default features)
run: cargo build --no-default-features
- name: Build (std, no download)
run: cargo build --no-default-features --features std
- name: Build (all features)
run: cargo build --all-features
- name: Test (all features)
Expand Down
7 changes: 5 additions & 2 deletions crates/bitcoind_rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@ workspace = true

[dependencies]
bitcoin = { version = "0.32.0", default-features = false }
bitcoincore-rpc = { version = "0.19.0" }
bitcoind_client = { package = "bdk_bitcoind_client", version = "0.2.0", default-features = false, features = ["bitreq", "28_0"] }
bdk_core = { path = "../core", version = "0.6.1", default-features = false }

[dev-dependencies]
bdk_bitcoind_rpc = { path = "." }
bdk_bitcoind_rpc = { path = ".", features = ["28_0"] }
bdk_testenv = { path = "../testenv" }
bdk_chain = { path = "../chain" }

[features]
default = ["std"]
std = ["bitcoin/std", "bdk_core/std"]
serde = ["bitcoin/serde", "bdk_core/serde"]
28_0 = ["bitcoind_client/28_0"]
29_0 = ["bitcoind_client/29_0"]
30_0 = ["bitcoind_client/30_0"]

[[example]]
name = "filter_iter"
Expand Down
61 changes: 60 additions & 1 deletion crates/bitcoind_rpc/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
# BDK Bitcoind RPC

This crate is used for emitting blockchain data from the `bitcoind` RPC interface.
This crate emits blockchain data from the `bitcoind` RPC interface. It does not use the wallet
RPC API, so it works against wallet-disabled Bitcoin Core nodes.

[`Emitter`] is the main entry point. It sources blocks and mempool transactions from a
[`bitcoind_client::bitreq::Client`] and produces updates that connect to a local chain you already
hold, handling reorgs along the way.

## Usage

Give the emitter a checkpoint describing the chain you already know about (for a fresh wallet this
is just the genesis block). Then:

- Call [`Emitter::next_block`] in a loop until it returns `Ok(None)` to walk the chain forward,
block by block, up to the node's tip. Each [`BlockEvent`] carries a [`CheckPoint`] that connects
to your existing chain, so it can be applied directly to a `LocalChain`.
- Call [`Emitter::mempool`] to emit the current mempool: the full set of unconfirmed transactions
plus any that have been evicted since the last call.

If your wallet has a known creation height ("birthday"), use [`Emitter::start_height`] to skip
directly to it instead of scanning from the checkpoint height.

```rust,no_run
use bdk_bitcoind_rpc::{Emitter, EmitterError};
use bdk_chain::local_chain::LocalChain;
use bitcoin::{constants::genesis_block, BlockHash, Network, Transaction};
use bitcoind_client::bitreq::{Auth, Client};

// The chain we already know about. A fresh wallet starts at the network's genesis block.
let genesis_hash: BlockHash = genesis_block(Network::Bitcoin).block_hash();
let (local_chain, _) = LocalChain::from_genesis(genesis_hash);

let rpc_client = Client::with_auth(
"127.0.0.1:8332",
Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
)?;

let mut emitter = Emitter::new(
&rpc_client,
local_chain.tip(),
core::iter::empty::<Transaction>(),
);

// Walk the chain forward until the node's tip is reached.
while let Some(event) = emitter.next_block()? {
println!("block {}: {}", event.block_height(), event.block_hash());
}

// Emit the current mempool state.
let mempool = emitter.mempool()?;
println!("{} mempool txs, {} evicted", mempool.update.len(), mempool.evicted.len());
# <Result<_, EmitterError>>::Ok(())
```

[`Emitter`]: crate::Emitter
[`Emitter::next_block`]: crate::Emitter::next_block
[`Emitter::mempool`]: crate::Emitter::mempool
[`Emitter::start_height`]: crate::Emitter::start_height
[`BlockEvent`]: crate::BlockEvent
[`CheckPoint`]: bdk_core::CheckPoint
[`bitcoind_client::bitreq::Client`]: bitcoind_client::bitreq::Client
6 changes: 4 additions & 2 deletions crates/bitcoind_rpc/examples/filter_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ fn main() -> anyhow::Result<()> {
// Configure RPC client
let url = std::env::var("RPC_URL").context("must set RPC_URL")?;
let cookie = std::env::var("RPC_COOKIE").context("must set RPC_COOKIE")?;
let rpc_client =
bitcoincore_rpc::Client::new(&url, bitcoincore_rpc::Auth::CookieFile(cookie.into()))?;
let rpc_client = bitcoind_client::bitreq::Client::with_auth(
&url,
bitcoind_client::bitreq::Auth::CookieFile(cookie.into()),
)?;

// Initialize `FilterIter`
let mut spks = vec![];
Expand Down
96 changes: 53 additions & 43 deletions crates/bitcoind_rpc/src/bip158.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
//! [0]: https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki
//! [1]: https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki

use core::fmt::{self, Debug, Display};

use bdk_core::bitcoin;
use bdk_core::CheckPoint;
use bitcoin::BlockHash;
use bdk_core::{CheckPoint, ToBlockHash};
use bitcoin::{bip158::BlockFilter, Block, ScriptBuf};
use bitcoincore_rpc;
use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
use bitcoin::{block::Header, hashes::Hash, BlockHash};
use bitcoind_client::bitreq::Client;

use crate::corepc_types::model::GetBlockHeaderVerbose;

/// Type that returns Bitcoin blocks by matching a list of script pubkeys (SPKs) against a
/// [`bip158::BlockFilter`](bitcoin::bip158::BlockFilter).
///
/// * `FilterIter` talks to bitcoind via JSON-RPC interface, which is handled by the
/// [`bitcoincore_rpc::Client`].
/// [`bitcoind_client::bitreq::Client`].
/// * Collect the script pubkeys (SPKs) you want to watch. These will usually correspond to wallet
/// addresses that have been handed out for receiving payments.
/// * Construct `FilterIter` with the RPC client, SPKs, and [`CheckPoint`]. The checkpoint tip
Expand All @@ -29,22 +32,22 @@ use bitcoincore_rpc::{json::GetBlockHeaderResult, RpcApi};
/// Events contain the updated checkpoint `cp` which may be incorporated into the local chain
/// state to stay in sync with the tip.
#[derive(Debug)]
pub struct FilterIter<'a> {
pub struct FilterIter<'a, B> {
/// RPC client
client: &'a bitcoincore_rpc::Client,
client: &'a Client,
/// SPK inventory
spks: Vec<ScriptBuf>,
/// checkpoint
cp: CheckPoint<BlockHash>,
cp: CheckPoint<B>,
/// Header info, contains the prev and next hashes for each header.
header: Option<GetBlockHeaderResult>,
header: Option<GetBlockHeaderVerbose>,
}

impl<'a> FilterIter<'a> {
impl<'a, B> FilterIter<'a, B> {
/// Construct [`FilterIter`] with checkpoint, RPC client and SPKs.
pub fn new(
client: &'a bitcoincore_rpc::Client,
cp: CheckPoint,
client: &'a Client,
cp: CheckPoint<B>,
spks: impl IntoIterator<Item = ScriptBuf>,
) -> Self {
Self {
Expand All @@ -58,10 +61,10 @@ impl<'a> FilterIter<'a> {
/// Return the agreement header with the remote node.
///
/// Error if no agreement header is found.
fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
fn find_base(&self) -> Result<GetBlockHeaderVerbose, Error> {
for cp in self.cp.iter() {
match self.client.get_block_header_info(&cp.hash()) {
Err(e) if is_not_found(&e) => continue,
match self.client.get_block_header_verbose(&cp.hash()) {
Err(e) if e.is_not_found_error() => continue,
Ok(header) if header.confirmations <= 0 => continue,
Ok(header) => return Ok(header),
Err(e) => return Err(Error::Rpc(e)),
Expand All @@ -73,14 +76,14 @@ impl<'a> FilterIter<'a> {

/// Event returned by [`FilterIter`].
#[derive(Debug, Clone)]
pub struct Event {
pub struct Event<B> {
/// Checkpoint
pub cp: CheckPoint,
pub cp: CheckPoint<B>,
/// Block, will be `Some(..)` for matching blocks
pub block: Option<Block>,
}

impl Event {
impl<B> Event<B> {
/// Whether this event contains a matching block.
pub fn is_match(&self) -> bool {
self.block.is_some()
Expand All @@ -92,8 +95,11 @@ impl Event {
}
}

impl Iterator for FilterIter<'_> {
type Item = Result<Event, Error>;
impl<B> Iterator for FilterIter<'_, B>
where
B: ToBlockHash + Debug + Clone + From<Header>,
{
type Item = Result<Event<B>, Error>;

fn next(&mut self) -> Option<Self::Item> {
(|| -> Result<Option<_>, Error> {
Expand All @@ -111,22 +117,22 @@ impl Iterator for FilterIter<'_> {
None => return Ok(None),
};

let mut next_header = self.client.get_block_header_info(&next_hash)?;
let mut next_header = self.client.get_block_header_verbose(&next_hash)?;

// In case of a reorg, rewind by fetching headers of previous hashes until we find
// one with enough confirmations.
while next_header.confirmations < 0 {
let prev_hash = next_header
.previous_block_hash
.ok_or(Error::ReorgDepthExceeded)?;
let prev_header = self.client.get_block_header_info(&prev_hash)?;
let prev_header = self.client.get_block_header_verbose(&prev_hash)?;
next_header = prev_header;
}

next_hash = next_header.hash;
let next_height: u32 = next_header.height.try_into()?;
let next_height = next_header.height;

cp = cp.insert(next_height, next_hash);
cp = cp.insert(next_height, next_header.as_header().into());

let mut block = None;
let filter =
Expand All @@ -151,47 +157,51 @@ impl Iterator for FilterIter<'_> {

/// Error that may be thrown by [`FilterIter`].
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// RPC error
Rpc(bitcoincore_rpc::Error),
Rpc(bitcoind_client::Error),
/// `bitcoin::bip158` error
Bip158(bitcoin::bip158::Error),
/// Max reorg depth exceeded.
ReorgDepthExceeded,
/// Error converting an integer
TryFromInt(core::num::TryFromIntError),
}

impl core::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Rpc(e) => write!(f, "{e}"),
Self::Bip158(e) => write!(f, "{e}"),
Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"),
Self::TryFromInt(e) => write!(f, "{e}"),
}
}
}

impl core::error::Error for Error {}

impl From<bitcoincore_rpc::Error> for Error {
fn from(e: bitcoincore_rpc::Error) -> Self {
impl From<bitcoind_client::Error> for Error {
fn from(e: bitcoind_client::Error) -> Self {
Self::Rpc(e)
}
}

impl From<core::num::TryFromIntError> for Error {
fn from(e: core::num::TryFromIntError) -> Self {
Self::TryFromInt(e)
}
/// Trait used internally to derive a Bitcoin block [`Header`] from an instance of
/// [`GetBlockHeaderVerbose`].
trait AsHeader {
fn as_header(&self) -> Header;
}

/// Whether the RPC error is a "not found" error (code: `-5`).
fn is_not_found(e: &bitcoincore_rpc::Error) -> bool {
matches!(
e,
bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e))
if e.code == -5
)
impl AsHeader for GetBlockHeaderVerbose {
fn as_header(&self) -> Header {
Header {
version: self.version,
prev_blockhash: self
.previous_block_hash
.unwrap_or(BlockHash::from_byte_array([0x00; 32])),
merkle_root: self.merkle_root,
time: self.time,
bits: self.bits,
nonce: self.nonce,
}
}
}
Loading