Skip to content

Commit 64cdf71

Browse files
committed
wip
1 parent d011dac commit 64cdf71

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
#![allow(clippy::print_stdout, clippy::print_stderr)]
2+
use std::time::Instant;
3+
4+
use anyhow::Context;
5+
use bdk_chain::bitcoin::{
6+
bip158::BlockFilter, secp256k1::Secp256k1, Block, BlockHash,
7+
Network, ScriptBuf,
8+
};
9+
use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
10+
use bdk_chain::miniscript::Descriptor;
11+
use bdk_chain::{
12+
Anchor, BlockId, CanonicalizationParams, CanonicalizationTask, ChainOracle,
13+
ConfirmationBlockTime, IndexedTxGraph, SpkIterator,
14+
};
15+
use bdk_testenv::anyhow;
16+
use bitcoincore_rpc::json::GetBlockHeaderResult;
17+
use bitcoincore_rpc::{Client, RpcApi};
18+
19+
// This example shows how to use a CoreOracle that implements ChainOracle trait
20+
// to handle canonicalization with bitcoind RPC, without needing LocalChain.
21+
22+
const EXTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/0/*)";
23+
const INTERNAL: &str = "tr([83737d5e/86'/1'/0']tpubDDR5GgtoxS8fJyjjvdahN4VzV5DV6jtbcyvVXhEKq2XtpxjxBXmxH3r8QrNbQqHg4bJM1EGkxi7Pjfkgnui9jQWqS7kxHvX6rhUeriLDKxz/1/*)";
24+
const SPK_COUNT: u32 = 25;
25+
const NETWORK: Network = Network::Signet;
26+
27+
const START_HEIGHT: u32 = 205_000;
28+
const START_HASH: &str = "0000002bd0f82f8c0c0f1e19128f84c938763641dba85c44bdb6aed1678d16cb";
29+
30+
/// Error types for CoreOracle and FilterIterV2
31+
#[derive(Debug)]
32+
pub enum Error {
33+
/// RPC error
34+
Rpc(bitcoincore_rpc::Error),
35+
/// `bitcoin::bip158` error
36+
Bip158(bdk_chain::bitcoin::bip158::Error),
37+
/// Max reorg depth exceeded
38+
ReorgDepthExceeded,
39+
/// Error converting an integer
40+
TryFromInt(core::num::TryFromIntError),
41+
}
42+
43+
impl core::fmt::Display for Error {
44+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45+
match self {
46+
Self::Rpc(e) => write!(f, "{e}"),
47+
Self::Bip158(e) => write!(f, "{e}"),
48+
Self::ReorgDepthExceeded => write!(f, "maximum reorg depth exceeded"),
49+
Self::TryFromInt(e) => write!(f, "{e}"),
50+
}
51+
}
52+
}
53+
54+
impl std::error::Error for Error {}
55+
56+
impl From<bitcoincore_rpc::Error> for Error {
57+
fn from(e: bitcoincore_rpc::Error) -> Self {
58+
Self::Rpc(e)
59+
}
60+
}
61+
62+
impl From<core::num::TryFromIntError> for Error {
63+
fn from(e: core::num::TryFromIntError) -> Self {
64+
Self::TryFromInt(e)
65+
}
66+
}
67+
68+
impl From<bdk_chain::bitcoin::bip158::Error> for Error {
69+
fn from(e: bdk_chain::bitcoin::bip158::Error) -> Self {
70+
Self::Bip158(e)
71+
}
72+
}
73+
74+
/// Whether the RPC error is a "not found" error (code: `-5`)
75+
fn is_not_found(e: &bitcoincore_rpc::Error) -> bool {
76+
matches!(
77+
e,
78+
bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(e))
79+
if e.code == -5
80+
)
81+
}
82+
83+
/// CoreOracle implements ChainOracle using bitcoind RPC
84+
pub struct CoreOracle {
85+
client: Client,
86+
cached_tip: Option<BlockId>,
87+
}
88+
89+
impl CoreOracle {
90+
pub fn new(client: Client) -> Self {
91+
Self {
92+
client,
93+
cached_tip: None,
94+
}
95+
}
96+
97+
/// Refresh and return the chain tip
98+
pub fn refresh_tip(&mut self) -> Result<BlockId, Error> {
99+
let height = self.client.get_block_count()? as u32;
100+
let hash = self.client.get_block_hash(height as u64)?;
101+
let tip = BlockId { height, hash };
102+
self.cached_tip = Some(tip);
103+
Ok(tip)
104+
}
105+
106+
/// Canonicalize a transaction graph using this oracle
107+
pub fn canonicalize<A: Anchor>(
108+
&self,
109+
mut task: CanonicalizationTask<'_, A>,
110+
chain_tip: BlockId,
111+
) -> bdk_chain::CanonicalView<A> {
112+
// Process all queries from the task
113+
while let Some(request) = task.next_query() {
114+
// Check each anchor against the chain
115+
let mut best_anchor = None;
116+
117+
for anchor in &request.anchors {
118+
let block_id = anchor.anchor_block();
119+
120+
// Check if block is in chain
121+
match self.is_block_in_chain(block_id, chain_tip) {
122+
Ok(Some(true)) => {
123+
best_anchor = Some(anchor.clone());
124+
break; // Found a confirmed anchor
125+
}
126+
_ => continue, // Not confirmed or error, check next
127+
}
128+
}
129+
130+
task.resolve_query(best_anchor);
131+
}
132+
133+
// Finish and return the canonical view
134+
task.finish(chain_tip)
135+
}
136+
}
137+
138+
impl ChainOracle for CoreOracle {
139+
type Error = Error;
140+
141+
fn is_block_in_chain(
142+
&self,
143+
block: BlockId,
144+
chain_tip: BlockId,
145+
) -> Result<Option<bool>, Self::Error> {
146+
// Check if the requested block height is within range
147+
if block.height > chain_tip.height {
148+
return Ok(Some(false));
149+
}
150+
151+
// Get the block hash at the requested height
152+
match self.client.get_block_hash(block.height as u64) {
153+
Ok(hash_at_height) => Ok(Some(hash_at_height == block.hash)),
154+
Err(e) if is_not_found(&e) => Ok(Some(false)),
155+
Err(_) => Ok(None), // Can't determine, return None
156+
}
157+
}
158+
159+
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
160+
if let Some(tip) = self.cached_tip {
161+
Ok(tip)
162+
} else {
163+
let height = self.client.get_block_count()? as u32;
164+
let hash = self.client.get_block_hash(height as u64)?;
165+
Ok(BlockId { height, hash })
166+
}
167+
}
168+
}
169+
170+
/// FilterIterV2: Similar to FilterIter but doesn't manage CheckPoints
171+
pub struct FilterIterV2<'a> {
172+
client: &'a Client,
173+
spks: Vec<ScriptBuf>,
174+
current_height: u32,
175+
current_hash: BlockHash,
176+
header: Option<GetBlockHeaderResult>,
177+
}
178+
179+
impl<'a> FilterIterV2<'a> {
180+
pub fn new(
181+
client: &'a Client,
182+
start_height: u32,
183+
start_hash: BlockHash,
184+
spks: impl IntoIterator<Item = ScriptBuf>,
185+
) -> Self {
186+
Self {
187+
client,
188+
spks: spks.into_iter().collect(),
189+
current_height: start_height,
190+
current_hash: start_hash,
191+
header: None,
192+
}
193+
}
194+
195+
/// Find the starting point for iteration
196+
fn find_base(&self) -> Result<GetBlockHeaderResult, Error> {
197+
match self.client.get_block_header_info(&self.current_hash) {
198+
Ok(header) if header.confirmations > 0 => Ok(header),
199+
_ => {
200+
// If we can't find the starting hash, try to get the header at the height
201+
let hash = self.client.get_block_hash(self.current_height as u64)?;
202+
Ok(self.client.get_block_header_info(&hash)?)
203+
}
204+
}
205+
}
206+
}
207+
208+
/// Event returned by FilterIterV2
209+
#[derive(Debug, Clone)]
210+
pub struct EventV2 {
211+
pub height: u32,
212+
pub hash: BlockHash,
213+
pub block: Option<Block>,
214+
}
215+
216+
impl Iterator for FilterIterV2<'_> {
217+
type Item = Result<EventV2, Error>;
218+
219+
fn next(&mut self) -> Option<Self::Item> {
220+
let result = (|| -> Result<Option<EventV2>, Error> {
221+
let header = match self.header.take() {
222+
Some(header) => header,
223+
None => self.find_base()?,
224+
};
225+
226+
let next_hash = match header.next_block_hash {
227+
Some(hash) => hash,
228+
None => return Ok(None), // Reached chain tip
229+
};
230+
231+
let mut next_header = self.client.get_block_header_info(&next_hash)?;
232+
233+
// Handle reorgs
234+
while next_header.confirmations < 0 {
235+
let prev_hash = next_header
236+
.previous_block_hash
237+
.ok_or(Error::ReorgDepthExceeded)?;
238+
next_header = self.client.get_block_header_info(&prev_hash)?;
239+
}
240+
241+
let height = next_header.height.try_into()?;
242+
let hash = next_header.hash;
243+
244+
// Check if block matches our filters
245+
let mut block = None;
246+
let filter = BlockFilter::new(
247+
self.client
248+
.get_block_filter(&hash)?
249+
.filter
250+
.as_slice(),
251+
);
252+
253+
if filter.match_any(&hash, self.spks.iter().map(ScriptBuf::as_ref))? {
254+
block = Some(self.client.get_block(&hash)?);
255+
}
256+
257+
// Update state
258+
self.current_height = height;
259+
self.current_hash = hash;
260+
self.header = Some(next_header);
261+
262+
Ok(Some(EventV2 {
263+
height,
264+
hash,
265+
block,
266+
}))
267+
})();
268+
269+
result.transpose()
270+
}
271+
}
272+
273+
fn main() -> anyhow::Result<()> {
274+
// Setup descriptors and graph
275+
let secp = Secp256k1::new();
276+
let (descriptor, _) = Descriptor::parse_descriptor(&secp, EXTERNAL)?;
277+
let (change_descriptor, _) = Descriptor::parse_descriptor(&secp, INTERNAL)?;
278+
279+
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<&str>>::new({
280+
let mut index = KeychainTxOutIndex::default();
281+
index.insert_descriptor("external", descriptor.clone())?;
282+
index.insert_descriptor("internal", change_descriptor.clone())?;
283+
index
284+
});
285+
286+
// Configure RPC client
287+
let url = std::env::var("RPC_URL").context("must set RPC_URL")?;
288+
let cookie = std::env::var("RPC_COOKIE").context("must set RPC_COOKIE")?;
289+
let client = Client::new(&url, bitcoincore_rpc::Auth::CookieFile(cookie.into()))?;
290+
291+
// Generate SPKs to watch
292+
let mut spks = vec![];
293+
for (_, desc) in graph.index.keychains() {
294+
spks.extend(SpkIterator::new_with_range(desc, 0..SPK_COUNT).map(|(_, s)| s));
295+
}
296+
297+
// Create FilterIterV2
298+
let iter = FilterIterV2::new(
299+
&client,
300+
START_HEIGHT,
301+
START_HASH.parse()?,
302+
spks,
303+
);
304+
305+
let start = Instant::now();
306+
let mut last_height = START_HEIGHT;
307+
308+
// Scan blocks
309+
println!("Scanning blocks...");
310+
for res in iter {
311+
let event = res?;
312+
last_height = event.height;
313+
314+
if let Some(block) = event.block {
315+
let _ = graph.apply_block_relevant(&block, event.height);
316+
println!("Matched block at height {}", event.height);
317+
}
318+
}
319+
320+
println!("\nScan took: {}s", start.elapsed().as_secs());
321+
println!("Scanned up to height: {}", last_height);
322+
323+
// Create CoreOracle
324+
let mut oracle = CoreOracle::new(client);
325+
326+
// Get current chain tip from oracle
327+
let chain_tip = oracle.refresh_tip()?;
328+
println!("Chain tip: height={}, hash={}", chain_tip.height, chain_tip.hash);
329+
330+
// Perform canonicalization using CoreOracle
331+
println!("\nPerforming canonicalization using CoreOracle...");
332+
let task = graph.canonicalization_task(CanonicalizationParams::default());
333+
let canonical_view = oracle.canonicalize(task, chain_tip);
334+
335+
println!("Canonical view created:");
336+
// println!(" Chain tip: {:?}", canonical_view.chain_tip());
337+
338+
// Calculate balance
339+
let balance = canonical_view.balance(
340+
graph.index.outpoints().iter().cloned(),
341+
|(k, _), _| k == &"external" || k == &"internal",
342+
0,
343+
);
344+
345+
println!("\nBalance:");
346+
println!(" Confirmed: {} sats", balance.confirmed);
347+
// println!(" Unconfirmed: {} sats", balance.unconfirmed);
348+
println!(" Total: {} sats", balance.total());
349+
350+
// Display unspent outputs
351+
let unspent: Vec<_> = canonical_view
352+
.filter_unspent_outpoints(graph.index.outpoints().clone())
353+
.collect();
354+
355+
if !unspent.is_empty() {
356+
println!("\nUnspent outputs:");
357+
for (index, utxo) in unspent {
358+
println!(" {:?} | {} sats | {}", index, utxo.txout.value, utxo.outpoint);
359+
}
360+
}
361+
362+
// Display canonical transactions
363+
let canonical_txs: Vec<_> = canonical_view.txs().collect();
364+
println!("\nCanonical transactions: {}", canonical_txs.len());
365+
366+
for tx in &canonical_txs {
367+
match &tx.pos {
368+
bdk_chain::ChainPosition::Confirmed { anchor, .. } => {
369+
let block_id = anchor.anchor_block();
370+
println!(" {} - Confirmed at height {}", tx.txid, block_id.height);
371+
}
372+
bdk_chain::ChainPosition::Unconfirmed { last_seen, .. } => {
373+
println!(" {} - Unconfirmed (last seen: {:?})", tx.txid, last_seen);
374+
}
375+
}
376+
}
377+
378+
Ok(())
379+
}

0 commit comments

Comments
 (0)