Skip to content
Open
Show file tree
Hide file tree
Changes from all 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