Skip to content

Commit 3a9302f

Browse files
committed
docs(examples): add FilterItervV2 w/o LocalChain dependency
1 parent 9e27ab1 commit 3a9302f

File tree

1 file changed

+302
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)