Skip to content

Commit 4a01624

Browse files
committed
docs(examples): add ChainQuery examples for sans-io canonicalization
Add `example_chain_query` package with two implementations demonstrating the new `ChainQuery` trait usage without full chain storage: - rpc_oracle: Bitcoin Core RPC with synchronous ChainOracle trait - kyoto_oracle: BIP157/158 compact filters with async Kyoto client Both examples showcase `CanonicalizationTask` API for lightweight chain verification using on-demand block queries instead of storing all headers.
1 parent 9e27ab1 commit 4a01624

File tree

5 files changed

+650
-0
lines changed

5 files changed

+650
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"examples/example_electrum",
1313
"examples/example_esplora",
1414
"examples/example_bitcoind_rpc_polling",
15+
"examples/example_chain_query",
1516
]
1617

1718
[workspace.package]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "example_chain_query"
3+
version = "0.1.0"
4+
edition = "2021"
5+
rust-version = "1.84"
6+
7+
[dependencies]
8+
bdk_chain = { path = "../../crates/chain" }
9+
bdk_core = { path = "../../crates/core" }
10+
bitcoin = { version = "0.32.0" }
11+
bitcoincore-rpc = { version = "0.19.0" }
12+
bip157 = { version = "0.3.2" }
13+
tokio = { version = "1.19", features = ["rt-multi-thread", "macros"] }
14+
anyhow = "1"
15+
tracing = "0.1"
16+
tracing-subscriber = "0.3"
17+
18+
[[bin]]
19+
name = "kyoto_oracle"
20+
path = "bin/kyoto_oracle.rs"
21+
22+
[[bin]]
23+
name = "bitcoind_rpc_oracle"
24+
path = "bin/bitcoind_rpc_oracle.rs"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# ChainQuery Examples
2+
3+
This directory contains examples demonstrating the use of BDK's `ChainQuery` trait for transaction canonicalization without requiring a full local chain store.
4+
5+
## Examples
6+
7+
### bitcoind_rpc_oracle
8+
Uses Bitcoin Core RPC with the `ChainOracle` trait implementation to perform on-demand block verification during canonicalization.
9+
10+
#### Setup for Signet
11+
12+
1. Start local signet bitcoind (~8 GB space required):
13+
```bash
14+
mkdir -p /tmp/signet/bitcoind
15+
bitcoind -signet -server -fallbackfee=0.0002 -blockfilterindex -datadir=/tmp/signet/bitcoind -daemon
16+
tail -f /tmp/signet/bitcoind/signet/debug.log
17+
```
18+
Watch debug.log and wait for bitcoind to finish syncing.
19+
20+
2. Set bitcoind environment variables:
21+
```bash
22+
export RPC_URL=127.0.0.1:38332
23+
export RPC_COOKIE=/tmp/signet/bitcoind/signet/.cookie
24+
```
25+
26+
3. Run the example:
27+
```bash
28+
cargo run --bin bitcoind_rpc_oracle
29+
```
30+
31+
### kyoto_oracle
32+
Uses Kyoto (BIP157/158 compact block filters) with async on-demand block fetching for canonicalization. Connects to Signet network peers.
33+
34+
To run:
35+
```bash
36+
cargo run --bin kyoto_oracle
37+
```
38+
39+
## Key Concepts
40+
41+
Both examples demonstrate:
42+
- Using `CanonicalizationTask` with the `ChainQuery` trait
43+
- On-demand chain data fetching instead of storing all headers locally
44+
- Processing transaction graphs without a full `LocalChain`
45+
46+
The main difference is the backend:
47+
- `bitcoind_rpc_oracle`: Synchronous RPC calls to Bitcoin Core
48+
- `kyoto_oracle`: Async P2P network communication using compact block filters
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#![allow(clippy::print_stdout, clippy::print_stderr)]
2+
use std::time::Instant;
3+
4+
use anyhow::Context;
5+
use bdk_chain::bitcoin::{bip158::BlockFilter, secp256k1::Secp256k1, Block, ScriptBuf};
6+
use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
7+
use bdk_chain::miniscript::Descriptor;
8+
use bdk_chain::{
9+
Anchor, BlockId, CanonicalizationParams, CanonicalizationTask, ChainOracle, ChainQuery,
10+
ConfirmationBlockTime, IndexedTxGraph, SpkIterator,
11+
};
12+
use bitcoincore_rpc::json::GetBlockHeaderResult;
13+
use bitcoincore_rpc::{Client, RpcApi};
14+
15+
// This example shows how to use a CoreOracle that implements ChainOracle trait
16+
// to handle canonicalization with bitcoind RPC, without needing LocalChain.
17+
18+
const EXTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)";
19+
const INTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)";
20+
const SPK_COUNT: u32 = 25;
21+
22+
const START_HEIGHT: u32 = 205_000;
23+
24+
/// Error types for CoreOracle and FilterIterV2
25+
#[derive(Debug)]
26+
pub enum Error {
27+
/// RPC error
28+
Rpc(bitcoincore_rpc::Error),
29+
/// `bitcoin::bip158` error
30+
Bip158(bdk_chain::bitcoin::bip158::Error),
31+
/// Max reorg depth exceeded
32+
ReorgDepthExceeded,
33+
/// Error converting an integer
34+
TryFromInt(core::num::TryFromIntError),
35+
}
36+
37+
impl core::fmt::Display for Error {
38+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39+
match self {
40+
Self::Rpc(e) => write!(f, "{e}"),
41+
Self::Bip158(e) => write!(f, "{e}"),
42+
Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"),
43+
Self::TryFromInt(e) => write!(f, "{e}"),
44+
}
45+
}
46+
}
47+
48+
impl std::error::Error for Error {}
49+
50+
impl From<bitcoincore_rpc::Error> for Error {
51+
fn from(e: bitcoincore_rpc::Error) -> Self {
52+
Self::Rpc(e)
53+
}
54+
}
55+
56+
impl From<core::num::TryFromIntError> for Error {
57+
fn from(e: core::num::TryFromIntError) -> Self {
58+
Self::TryFromInt(e)
59+
}
60+
}
61+
62+
impl From<bdk_chain::bitcoin::bip158::Error> for Error {
63+
fn from(e: bdk_chain::bitcoin::bip158::Error) -> Self {
64+
Self::Bip158(e)
65+
}
66+
}
67+
68+
/// Whether the RPC error is a "not found" error (code: `-5`)
69+
fn is_not_found(e: &bitcoincore_rpc::Error) -> bool {
70+
matches!(
71+
e,
72+
bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e))
73+
if e.code == -5
74+
)
75+
}
76+
77+
/// CoreOracle implements ChainOracle using bitcoind RPC
78+
pub struct CoreOracle {
79+
client: Client,
80+
}
81+
82+
impl CoreOracle {
83+
pub fn new(client: Client) -> Self {
84+
Self { client }
85+
}
86+
87+
/// Canonicalize a transaction graph using this oracle
88+
pub fn canonicalize<A: Anchor>(
89+
&self,
90+
mut task: CanonicalizationTask<'_, A>,
91+
chain_tip: BlockId,
92+
) -> bdk_chain::CanonicalView<A> {
93+
// Process all queries from the task
94+
while let Some(request) = task.next_query() {
95+
// Check each block_id against the chain to find the best one
96+
let mut best_block = None;
97+
98+
for block_id in &request.block_ids {
99+
// Check if block is in chain
100+
match self.is_block_in_chain(*block_id, chain_tip) {
101+
Ok(Some(true)) => {
102+
best_block = Some(*block_id);
103+
break; // Found a confirmed block
104+
}
105+
_ => continue, // Not confirmed or error, check next
106+
}
107+
}
108+
109+
task.resolve_query(best_block);
110+
}
111+
112+
// Finish and return the canonical view
113+
task.finish()
114+
}
115+
}
116+
117+
impl ChainOracle for CoreOracle {
118+
type Error = Error;
119+
120+
fn is_block_in_chain(
121+
&self,
122+
block: BlockId,
123+
chain_tip: BlockId,
124+
) -> Result<Option<bool>, Self::Error> {
125+
// Check if the requested block height is within range
126+
if block.height > chain_tip.height {
127+
return Ok(Some(false));
128+
}
129+
130+
// Get the block hash at the requested height
131+
match self.client.get_block_hash(block.height as u64) {
132+
Ok(hash_at_height) => Ok(Some(hash_at_height == block.hash)),
133+
Err(e) if is_not_found(&e) => Ok(Some(false)),
134+
Err(_) => Ok(None), // Can't determine, return None
135+
}
136+
}
137+
138+
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
139+
let height = self.client.get_block_count()? as u32;
140+
let hash = self.client.get_block_hash(height as u64)?;
141+
Ok(BlockId { height, hash })
142+
}
143+
}
144+
145+
/// FilterIterV2: Similar to FilterIter but doesn't manage CheckPoints
146+
pub struct FilterIterV2<'a> {
147+
client: &'a Client,
148+
spks: Vec<ScriptBuf>,
149+
current_height: u32,
150+
header: Option<GetBlockHeaderResult>,
151+
}
152+
153+
impl<'a> FilterIterV2<'a> {
154+
pub fn new(
155+
client: &'a Client,
156+
start_height: u32,
157+
spks: impl IntoIterator<Item = ScriptBuf>,
158+
) -> Self {
159+
Self {
160+
client,
161+
spks: spks.into_iter().collect(),
162+
current_height: start_height,
163+
header: None,
164+
}
165+
}
166+
167+
/// Find the starting point for iteration
168+
fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
169+
let hash = self.client.get_block_hash(self.current_height as u64)?;
170+
Ok(self.client.get_block_header_info(&hash)?)
171+
}
172+
}
173+
174+
/// Event returned by FilterIterV2 - contains a block that matches the filter
175+
#[derive(Debug, Clone)]
176+
pub struct EventV2 {
177+
pub block: Option<Block>,
178+
pub height: u32,
179+
}
180+
181+
impl Iterator for FilterIterV2<'_> {
182+
type Item = Result<EventV2, Error>;
183+
184+
fn next(&mut self) -> Option<Self::Item> {
185+
let result = (|| -> Result<Option<EventV2>, Error> {
186+
let header = match self.header.take() {
187+
Some(header) => header,
188+
None => self.find_base()?,
189+
};
190+
191+
let next_hash = match header.next_block_hash {
192+
Some(hash) => hash,
193+
None => return Ok(None), // Reached chain tip
194+
};
195+
196+
let mut next_header = self.client.get_block_header_info(&next_hash)?;
197+
198+
// Handle reorgs
199+
while next_header.confirmations < 0 {
200+
let prev_hash = next_header
201+
.previous_block_hash
202+
.ok_or(Error::ReorgDepthExceeded)?;
203+
next_header = self.client.get_block_header_info(&prev_hash)?;
204+
}
205+
206+
let height = next_header.height.try_into()?;
207+
let hash = next_header.hash;
208+
209+
// Check if block matches our filters
210+
let mut block = None;
211+
let filter = BlockFilter::new(self.client.get_block_filter(&hash)?.filter.as_slice());
212+
213+
if filter.match_any(&hash, self.spks.iter().map(ScriptBuf::as_ref))? {
214+
block = Some(self.client.get_block(&hash)?);
215+
}
216+
217+
// Update state
218+
self.current_height = height;
219+
self.header = Some(next_header);
220+
221+
Ok(Some(EventV2 { block, height }))
222+
})();
223+
224+
result.transpose()
225+
}
226+
}
227+
228+
fn main() -> anyhow::Result<()> {
229+
// Setup descriptors and graph
230+
let secp = Secp256k1::new();
231+
let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?;
232+
let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?;
233+
234+
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
235+
let mut index = KeychainTxOutIndex::default();
236+
index.insert_descriptor("external", descriptor.clone())?;
237+
index.insert_descriptor("internal", change_descriptor.clone())?;
238+
index
239+
});
240+
241+
// Configure RPC client
242+
let url = std::env::var("RPC_URL").context("must set RPC_URL")?;
243+
let cookie = std::env::var("RPC_COOKIE").context("must set RPC_COOKIE")?;
244+
let rpc_client = Client::new(&url, bitcoincore_rpc::Auth::CookieFile(cookie.into()))?;
245+
246+
// Initialize `FilterIter`
247+
let mut spks = vec![];
248+
for (_, desc) in graph.index.keychains() {
249+
spks.extend(SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, s)| s));
250+
}
251+
let iter = FilterIterV2::new(&rpc_client, START_HEIGHT, spks);
252+
253+
let start = Instant::now();
254+
255+
for res in iter {
256+
let event = res?;
257+
258+
if let Some(block) = event.block {
259+
let _ = graph.apply_block_relevant(&block, event.height);
260+
println!("Matched block {}", event.height);
261+
}
262+
}
263+
264+
println!("\ntook: {}s", start.elapsed().as_secs());
265+
266+
// Create `CoreOracle`
267+
let oracle = CoreOracle::new(rpc_client);
268+
269+
// Get current chain tip from `CoreOracle`
270+
let chain_tip = oracle.get_chain_tip()?;
271+
println!(
272+
"chain tip: height={}, hash={}",
273+
chain_tip.height, chain_tip.hash
274+
);
275+
276+
// Canonicalize TxGraph with `CoreCoracle`
277+
println!("\nPerforming canonicalization using CoreOracle...");
278+
let task = graph.canonicalization_task(chain_tip, CanonicalizationParams::default());
279+
let canonical_view = oracle.canonicalize(task, chain_tip);
280+
281+
// Display unspent outputs
282+
let unspent: Vec<_> = canonical_view
283+
.filter_unspent_outpoints(graph.index.outpoints().clone())
284+
.collect();
285+
286+
if !unspent.is_empty() {
287+
println!("\nUnspent");
288+
for (index, utxo) in unspent {
289+
// (k, index) | value | outpoint |
290+
println!("{:?} | {} | {}", index, utxo.txout.value, utxo.outpoint);
291+
}
292+
}
293+
294+
for canon_tx in canonical_view.txs() {
295+
if !canon_tx.pos.is_confirmed() {
296+
eprintln!("ERROR: canonical tx should be confirmed {}", canon_tx.txid);
297+
}
298+
}
299+
300+
Ok(())
301+
}

0 commit comments

Comments
 (0)