Skip to content

Commit ecd9199

Browse files
committed
wip
1 parent 9e27ab1 commit ecd9199

File tree

1 file changed

+307
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)