mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 14:54:47 -07:00
global: big snapshot
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "brk_rpc"
|
||||
description = "A thin wrapper around bitcoincore-rpc or corepc-client"
|
||||
description = "A thin wrapper around Bitcoin Core's JSON-RPC"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -8,20 +8,15 @@ homepage.workspace = true
|
||||
repository.workspace = true
|
||||
exclude = ["examples/"]
|
||||
|
||||
[features]
|
||||
default = ["corepc"]
|
||||
bitcoincore-rpc = ["dep:bitcoincore-rpc", "dep:serde_json", "brk_error/bitcoincore-rpc"]
|
||||
corepc = ["dep:corepc-client", "dep:corepc-jsonrpc", "dep:serde_json", "dep:serde", "brk_error/corepc"]
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { workspace = true }
|
||||
bitcoincore-rpc = { workspace = true, optional = true }
|
||||
corepc-client = { workspace = true, optional = true }
|
||||
corepc-jsonrpc = { workspace = true, optional = true }
|
||||
brk_error = { workspace = true }
|
||||
brk_error = { workspace = true, features = ["corepc", "serde_json"] }
|
||||
brk_logger = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
corepc-jsonrpc = { workspace = true }
|
||||
corepc-types = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
//! Compares results from the bitcoincore-rpc and corepc backends.
|
||||
//!
|
||||
//! Run with:
|
||||
//! cargo run -p brk_rpc --example compare_backends --features corepc
|
||||
|
||||
#[cfg(all(feature = "bitcoincore-rpc", feature = "corepc"))]
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(not(all(feature = "bitcoincore-rpc", feature = "corepc")))]
|
||||
fn main() {
|
||||
eprintln!("This example requires both features: --features bitcoincore-rpc,corepc");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "bitcoincore-rpc", feature = "corepc"))]
|
||||
fn main() {
|
||||
use brk_rpc::backend::{self, Auth};
|
||||
|
||||
brk_logger::init(None).unwrap();
|
||||
|
||||
let bitcoin_dir = brk_rpc::Client::default_bitcoin_path();
|
||||
let auth = Auth::CookieFile(bitcoin_dir.join(".cookie"));
|
||||
let url = brk_rpc::Client::default_url();
|
||||
|
||||
let bc = backend::bitcoincore::ClientInner::new(url, auth.clone(), 10, Duration::from_secs(1))
|
||||
.expect("bitcoincore client");
|
||||
let cp = backend::corepc::ClientInner::new(url, auth, 10, Duration::from_secs(1))
|
||||
.expect("corepc client");
|
||||
|
||||
println!("=== Comparing backends ===\n");
|
||||
|
||||
// --- get_blockchain_info ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_blockchain_info());
|
||||
let (t2, r2) = timed(|| cp.get_blockchain_info());
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_blockchain_info:");
|
||||
println!(
|
||||
" bitcoincore: headers={} blocks={} ({t1:?})",
|
||||
r1.headers, r1.blocks
|
||||
);
|
||||
println!(
|
||||
" corepc: headers={} blocks={} ({t2:?})",
|
||||
r2.headers, r2.blocks
|
||||
);
|
||||
assert_eq!(r1.headers, r2.headers, "headers mismatch");
|
||||
assert_eq!(r1.blocks, r2.blocks, "blocks mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block_count ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block_count());
|
||||
let (t2, r2) = timed(|| cp.get_block_count());
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_block_count:");
|
||||
println!(" bitcoincore: {r1} ({t1:?})");
|
||||
println!(" corepc: {r2} ({t2:?})");
|
||||
assert_eq!(r1, r2, "block count mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block_hash (height 0) ---
|
||||
let genesis_hash;
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block_hash(0));
|
||||
let (t2, r2) = timed(|| cp.get_block_hash(0));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
genesis_hash = r1;
|
||||
println!("get_block_hash(0):");
|
||||
println!(" bitcoincore: {r1} ({t1:?})");
|
||||
println!(" corepc: {r2} ({t2:?})");
|
||||
assert_eq!(r1, r2, "genesis hash mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block_header ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block_header(&genesis_hash));
|
||||
let (t2, r2) = timed(|| cp.get_block_header(&genesis_hash));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_block_header(genesis):");
|
||||
println!(" bitcoincore: prev={} ({t1:?})", r1.prev_blockhash);
|
||||
println!(" corepc: prev={} ({t2:?})", r2.prev_blockhash);
|
||||
assert_eq!(r1, r2, "header mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block_info ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block_info(&genesis_hash));
|
||||
let (t2, r2) = timed(|| cp.get_block_info(&genesis_hash));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_block_info(genesis):");
|
||||
println!(
|
||||
" bitcoincore: height={} confirmations={} ({t1:?})",
|
||||
r1.height, r1.confirmations
|
||||
);
|
||||
println!(
|
||||
" corepc: height={} confirmations={} ({t2:?})",
|
||||
r2.height, r2.confirmations
|
||||
);
|
||||
assert_eq!(r1.height, r2.height, "height mismatch");
|
||||
// confirmations can drift by 1 between calls
|
||||
assert!(
|
||||
(r1.confirmations - r2.confirmations).abs() <= 1,
|
||||
"confirmations mismatch: {} vs {}",
|
||||
r1.confirmations,
|
||||
r2.confirmations
|
||||
);
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block_header_info ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block_header_info(&genesis_hash));
|
||||
let (t2, r2) = timed(|| cp.get_block_header_info(&genesis_hash));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_block_header_info(genesis):");
|
||||
println!(
|
||||
" bitcoincore: height={} prev={:?} ({t1:?})",
|
||||
r1.height, r1.previous_block_hash
|
||||
);
|
||||
println!(
|
||||
" corepc: height={} prev={:?} ({t2:?})",
|
||||
r2.height, r2.previous_block_hash
|
||||
);
|
||||
assert_eq!(r1.height, r2.height, "height mismatch");
|
||||
assert_eq!(
|
||||
r1.previous_block_hash, r2.previous_block_hash,
|
||||
"prev hash mismatch"
|
||||
);
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_block (genesis) ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_block(&genesis_hash));
|
||||
let (t2, r2) = timed(|| cp.get_block(&genesis_hash));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_block(genesis):");
|
||||
println!(" bitcoincore: txs={} ({t1:?})", r1.txdata.len());
|
||||
println!(" corepc: txs={} ({t2:?})", r2.txdata.len());
|
||||
assert_eq!(r1, r2, "block mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_raw_mempool ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_raw_mempool());
|
||||
let (t2, r2) = timed(|| cp.get_raw_mempool());
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_raw_mempool:");
|
||||
println!(" bitcoincore: {} txs ({t1:?})", r1.len());
|
||||
println!(" corepc: {} txs ({t2:?})", r2.len());
|
||||
// Mempool can change between calls, just check they're reasonable
|
||||
println!(
|
||||
" {} (mempool is live, counts may differ slightly)\n",
|
||||
if r1.len() == r2.len() {
|
||||
"MATCH"
|
||||
} else {
|
||||
"CLOSE"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// --- get_raw_mempool_verbose ---
|
||||
{
|
||||
let (t1, r1) = timed(|| bc.get_raw_mempool_verbose());
|
||||
let (t2, r2) = timed(|| cp.get_raw_mempool_verbose());
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_raw_mempool_verbose:");
|
||||
println!(" bitcoincore: {} entries ({t1:?})", r1.len());
|
||||
println!(" corepc: {} entries ({t2:?})", r2.len());
|
||||
|
||||
// Compare a sample entry if both have data
|
||||
if let (Some((txid1, e1)), Some(_)) = (r1.first(), r2.first())
|
||||
&& let Some((_, e2)) = r2.iter().find(|(t, _)| t == txid1)
|
||||
{
|
||||
println!(" sample txid {txid1}:");
|
||||
println!(
|
||||
" bitcoincore: vsize={} fee={} ancestor_count={}",
|
||||
e1.vsize, e1.base_fee_sats, e1.ancestor_count
|
||||
);
|
||||
println!(
|
||||
" corepc: vsize={} fee={} ancestor_count={}",
|
||||
e2.vsize, e2.base_fee_sats, e2.ancestor_count
|
||||
);
|
||||
assert_eq!(e1.base_fee_sats, e2.base_fee_sats, "fee mismatch");
|
||||
assert_eq!(
|
||||
e1.ancestor_count, e2.ancestor_count,
|
||||
"ancestor_count mismatch"
|
||||
);
|
||||
println!(" MATCH");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// --- get_raw_transaction_hex (tx from block 1, genesis coinbase can't be retrieved) ---
|
||||
let block1_hash;
|
||||
{
|
||||
block1_hash = bc.get_block_hash(1).unwrap();
|
||||
let block = bc.get_block(&block1_hash).unwrap();
|
||||
let coinbase_txid = block.txdata[0].compute_txid();
|
||||
let (t1, r1) = timed(|| bc.get_raw_transaction_hex(&coinbase_txid, Some(&block1_hash)));
|
||||
let (t2, r2) = timed(|| cp.get_raw_transaction_hex(&coinbase_txid, Some(&block1_hash)));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_raw_transaction_hex(block 1 coinbase):");
|
||||
println!(" bitcoincore: {}... ({t1:?})", &r1[..40.min(r1.len())]);
|
||||
println!(" corepc: {}... ({t2:?})", &r2[..40.min(r2.len())]);
|
||||
assert_eq!(r1, r2, "raw tx hex mismatch");
|
||||
println!(" MATCH\n");
|
||||
}
|
||||
|
||||
// --- get_tx_out (genesis coinbase, likely unspendable but test the call) ---
|
||||
{
|
||||
let block = bc.get_block(&genesis_hash).unwrap();
|
||||
let coinbase_txid = block.txdata[0].compute_txid();
|
||||
let (t1, r1) = timed(|| bc.get_tx_out(&coinbase_txid, 0, Some(false)));
|
||||
let (t2, r2) = timed(|| cp.get_tx_out(&coinbase_txid, 0, Some(false)));
|
||||
let r1 = r1.unwrap();
|
||||
let r2 = r2.unwrap();
|
||||
println!("get_tx_out(genesis coinbase, vout=0):");
|
||||
match (&r1, &r2) {
|
||||
(Some(a), Some(b)) => {
|
||||
println!(
|
||||
" bitcoincore: coinbase={} value={:?} ({t1:?})",
|
||||
a.coinbase, a.value
|
||||
);
|
||||
println!(
|
||||
" corepc: coinbase={} value={:?} ({t2:?})",
|
||||
b.coinbase, b.value
|
||||
);
|
||||
assert_eq!(a.coinbase, b.coinbase, "coinbase mismatch");
|
||||
assert_eq!(a.value, b.value, "value mismatch");
|
||||
assert_eq!(a.script_pub_key, b.script_pub_key, "script mismatch");
|
||||
println!(" MATCH");
|
||||
}
|
||||
(None, None) => {
|
||||
println!(" both: None (spent) ({t1:?} / {t2:?})");
|
||||
println!(" MATCH");
|
||||
}
|
||||
_ => {
|
||||
println!(" MISMATCH: bitcoincore={r1:?}, corepc={r2:?}");
|
||||
panic!("get_tx_out mismatch");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("=== All checks passed ===");
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "bitcoincore-rpc", feature = "corepc"))]
|
||||
fn timed<T>(f: impl FnOnce() -> T) -> (Duration, T) {
|
||||
let start = Instant::now();
|
||||
let result = f();
|
||||
(start.elapsed(), result)
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use bitcoincore_rpc::{
|
||||
Client as CoreClient, Error as RpcError, RpcApi,
|
||||
json::{GetBlockTemplateCapabilities, GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
jsonrpc,
|
||||
};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{Sats, Txid};
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::value::RawValue;
|
||||
use tracing::info;
|
||||
|
||||
use super::{
|
||||
Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo,
|
||||
};
|
||||
|
||||
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
||||
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
||||
/// spend too long on a single batch before yielding results.
|
||||
const BATCH_CHUNK: usize = 2000;
|
||||
|
||||
fn to_rpc_auth(auth: &Auth) -> bitcoincore_rpc::Auth {
|
||||
match auth {
|
||||
Auth::None => bitcoincore_rpc::Auth::None,
|
||||
Auth::UserPass(u, p) => bitcoincore_rpc::Auth::UserPass(u.clone(), p.clone()),
|
||||
Auth::CookieFile(path) => bitcoincore_rpc::Auth::CookieFile(path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientInner {
|
||||
url: String,
|
||||
auth: Auth,
|
||||
client: RwLock<CoreClient>,
|
||||
max_retries: usize,
|
||||
retry_delay: Duration,
|
||||
}
|
||||
|
||||
impl ClientInner {
|
||||
pub fn new(url: &str, auth: Auth, max_retries: usize, retry_delay: Duration) -> Result<Self> {
|
||||
let rpc_auth = to_rpc_auth(&auth);
|
||||
let client = Self::retry(max_retries, retry_delay, || {
|
||||
CoreClient::new(url, rpc_auth.clone()).map_err(Into::into)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
auth,
|
||||
client: RwLock::new(client),
|
||||
max_retries,
|
||||
retry_delay,
|
||||
})
|
||||
}
|
||||
|
||||
fn recreate(&self) -> Result<()> {
|
||||
*self.client.write() = CoreClient::new(&self.url, to_rpc_auth(&self.auth))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_retriable(error: &RpcError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
RpcError::JsonRpc(jsonrpc::Error::Rpc(e))
|
||||
if e.code == -32600 || e.code == 401 || e.code == -28
|
||||
) || matches!(error, RpcError::JsonRpc(jsonrpc::Error::Transport(_)))
|
||||
}
|
||||
|
||||
fn retry<F, T>(max_retries: usize, delay: Duration, mut f: F) -> Result<T>
|
||||
where
|
||||
F: FnMut() -> Result<T>,
|
||||
{
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..=max_retries {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Retrying to connect to Bitcoin Core (attempt {}/{})",
|
||||
attempt, max_retries
|
||||
);
|
||||
sleep(delay);
|
||||
}
|
||||
|
||||
match f() {
|
||||
Ok(value) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Successfully connected to Bitcoin Core after {} retries",
|
||||
attempt
|
||||
);
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(e) => {
|
||||
if attempt == 0 {
|
||||
info!("Could not connect to Bitcoin Core, retrying: {}", e);
|
||||
}
|
||||
last_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let err = last_error.unwrap();
|
||||
info!(
|
||||
"Failed to connect to Bitcoin Core after {} attempts",
|
||||
max_retries + 1
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
|
||||
pub fn call_with_retry<F, T>(&self, f: F) -> Result<T, RpcError>
|
||||
where
|
||||
F: Fn(&CoreClient) -> Result<T, RpcError>,
|
||||
{
|
||||
for attempt in 0..=self.max_retries {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Trying to reconnect to Bitcoin Core (attempt {}/{})",
|
||||
attempt, self.max_retries
|
||||
);
|
||||
self.recreate().ok();
|
||||
sleep(self.retry_delay);
|
||||
}
|
||||
|
||||
match f(&self.client.read()) {
|
||||
Ok(value) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Successfully reconnected to Bitcoin Core after {} attempts",
|
||||
attempt
|
||||
);
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(e) if Self::is_retriable(&e) => {
|
||||
if attempt == 0 {
|
||||
info!("Lost connection to Bitcoin Core, reconnecting...");
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Could not reconnect to Bitcoin Core after {} attempts",
|
||||
self.max_retries + 1
|
||||
);
|
||||
Err(RpcError::JsonRpc(jsonrpc::Error::Rpc(
|
||||
jsonrpc::error::RpcError {
|
||||
code: -1,
|
||||
message: "Max retries exceeded".to_string(),
|
||||
data: None,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn call_once<F, T>(&self, f: F) -> Result<T, RpcError>
|
||||
where
|
||||
F: Fn(&CoreClient) -> Result<T, RpcError>,
|
||||
{
|
||||
f(&self.client.read())
|
||||
}
|
||||
|
||||
// --- Wrapped methods returning shared types ---
|
||||
|
||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_blockchain_info())?;
|
||||
Ok(BlockchainInfo {
|
||||
headers: r.headers,
|
||||
blocks: r.blocks,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block(&self, hash: &bitcoin::BlockHash) -> Result<bitcoin::Block> {
|
||||
Ok(self.call_with_retry(|c| c.get_block(hash))?)
|
||||
}
|
||||
|
||||
pub fn get_block_count(&self) -> Result<u64> {
|
||||
Ok(self.call_with_retry(|c| c.get_block_count())?)
|
||||
}
|
||||
|
||||
pub fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash> {
|
||||
Ok(self.call_with_retry(|c| c.get_block_hash(height))?)
|
||||
}
|
||||
|
||||
/// Batched canonical height → block hash lookup over the inclusive
|
||||
/// range `start..=end`. See the corepc backend for the rationale and
|
||||
/// chunking strategy; this mirror uses bitcoincore-rpc's
|
||||
/// `get_jsonrpc_client` accessor.
|
||||
pub fn get_block_hashes_range(
|
||||
&self,
|
||||
start: u64,
|
||||
end: u64,
|
||||
) -> Result<Vec<bitcoin::BlockHash>> {
|
||||
if end < start {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let total = (end - start + 1) as usize;
|
||||
let mut hashes = Vec::with_capacity(total);
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start <= end {
|
||||
let chunk_end = (chunk_start + BATCH_CHUNK as u64 - 1).min(end);
|
||||
self.batch_get_block_hashes(chunk_start, chunk_end, &mut hashes)?;
|
||||
chunk_start = chunk_end + 1;
|
||||
}
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
fn batch_get_block_hashes(
|
||||
&self,
|
||||
start: u64,
|
||||
end: u64,
|
||||
out: &mut Vec<bitcoin::BlockHash>,
|
||||
) -> Result<()> {
|
||||
let params: Vec<Box<RawValue>> = (start..=end)
|
||||
.map(|h| {
|
||||
RawValue::from_string(format!("[{h}]")).map_err(|e| Error::Parse(e.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let client = self.client.read();
|
||||
let jsonrpc_client = client.get_jsonrpc_client();
|
||||
let requests: Vec<jsonrpc::Request> = params
|
||||
.iter()
|
||||
.map(|p| jsonrpc_client.build_request("getblockhash", Some(p)))
|
||||
.collect();
|
||||
|
||||
let responses = jsonrpc_client
|
||||
.send_batch(&requests)
|
||||
.map_err(|e| Error::Parse(format!("getblockhash batch failed: {e}")))?;
|
||||
|
||||
for response in responses {
|
||||
let response = response.ok_or(Error::Internal("Missing response in JSON-RPC batch"))?;
|
||||
let hex: String = response
|
||||
.result()
|
||||
.map_err(|e| Error::Parse(format!("getblockhash batch result: {e}")))?;
|
||||
out.push(
|
||||
hex.parse::<bitcoin::BlockHash>()
|
||||
.map_err(|e| Error::Parse(format!("invalid block hash hex: {e}")))?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_block_header(&self, hash: &bitcoin::BlockHash) -> Result<bitcoin::block::Header> {
|
||||
Ok(self.call_with_retry(|c| c.get_block_header(hash))?)
|
||||
}
|
||||
|
||||
pub fn get_block_info(&self, hash: &bitcoin::BlockHash) -> Result<BlockInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_block_info(hash))?;
|
||||
Ok(BlockInfo {
|
||||
height: r.height,
|
||||
confirmations: r.confirmations as i64,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block_header_info(&self, hash: &bitcoin::BlockHash) -> Result<BlockHeaderInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_block_header_info(hash))?;
|
||||
Ok(BlockHeaderInfo {
|
||||
height: r.height,
|
||||
confirmations: r.confirmations as i64,
|
||||
previous_block_hash: r.previous_block_hash,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_tx_out(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
vout: u32,
|
||||
include_mempool: Option<bool>,
|
||||
) -> Result<Option<TxOutInfo>> {
|
||||
let r = self.call_with_retry(|c| c.get_tx_out(txid, vout, include_mempool))?;
|
||||
match r {
|
||||
Some(r) => Ok(Some(TxOutInfo {
|
||||
coinbase: r.coinbase,
|
||||
value: Sats::from(r.value.to_sat()),
|
||||
script_pub_key: r.script_pub_key.script()?,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<bitcoin::Txid>> {
|
||||
Ok(self.call_with_retry(|c| c.get_raw_mempool())?)
|
||||
}
|
||||
|
||||
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<(bitcoin::Txid, RawMempoolEntry)>> {
|
||||
let r = self.call_with_retry(|c| c.get_raw_mempool_verbose())?;
|
||||
Ok(r.into_iter()
|
||||
.map(|(txid, entry)| {
|
||||
(
|
||||
txid,
|
||||
RawMempoolEntry {
|
||||
vsize: entry.vsize,
|
||||
weight: entry.weight.unwrap_or(entry.vsize * 4),
|
||||
base_fee_sats: entry.fees.base.to_sat(),
|
||||
ancestor_count: entry.ancestor_count,
|
||||
ancestor_size: entry.ancestor_size,
|
||||
ancestor_fee_sats: entry.fees.ancestor.to_sat(),
|
||||
depends: entry.depends.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction_hex(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
block_hash: Option<&bitcoin::BlockHash>,
|
||||
) -> Result<String> {
|
||||
Ok(self.call_with_retry(|c| c.get_raw_transaction_hex(txid, block_hash))?)
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<bitcoin::Txid> {
|
||||
Ok(self.call_once(|c| c.send_raw_transaction(hex))?)
|
||||
}
|
||||
|
||||
/// Transactions Bitcoin Core would include in the next block it would
|
||||
/// mine. Core requires the `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
let r = self.call_with_retry(|c| {
|
||||
c.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[] as &[GetBlockTemplateCapabilities],
|
||||
)
|
||||
})?;
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee.to_sat()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{Sats, Txid};
|
||||
use corepc_client::client_sync::Auth as CorepcAuth;
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::value::RawValue;
|
||||
use tracing::info;
|
||||
|
||||
use super::{
|
||||
Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo,
|
||||
};
|
||||
|
||||
type CoreClient = corepc_client::client_sync::v30::Client;
|
||||
type CoreError = corepc_client::client_sync::Error;
|
||||
|
||||
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
||||
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
||||
/// spend too long on a single batch before yielding results.
|
||||
const BATCH_CHUNK: usize = 2000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientInner {
|
||||
url: String,
|
||||
auth: Auth,
|
||||
client: RwLock<CoreClient>,
|
||||
max_retries: usize,
|
||||
retry_delay: Duration,
|
||||
}
|
||||
|
||||
impl ClientInner {
|
||||
pub fn new(url: &str, auth: Auth, max_retries: usize, retry_delay: Duration) -> Result<Self> {
|
||||
let client = Self::retry(max_retries, retry_delay, || {
|
||||
Self::create_client(url, &auth).map_err(Into::into)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
auth,
|
||||
client: RwLock::new(client),
|
||||
max_retries,
|
||||
retry_delay,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_client(url: &str, auth: &Auth) -> Result<CoreClient, CoreError> {
|
||||
let corepc_auth = match auth {
|
||||
Auth::None => CorepcAuth::None,
|
||||
Auth::UserPass(u, p) => CorepcAuth::UserPass(u.clone(), p.clone()),
|
||||
Auth::CookieFile(path) => CorepcAuth::CookieFile(path.clone()),
|
||||
};
|
||||
match corepc_auth {
|
||||
CorepcAuth::None => Ok(CoreClient::new(url)),
|
||||
other => CoreClient::new_with_auth(url, other),
|
||||
}
|
||||
}
|
||||
|
||||
fn recreate(&self) -> Result<()> {
|
||||
*self.client.write() = Self::create_client(&self.url, &self.auth)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_retriable(error: &CoreError) -> bool {
|
||||
match error {
|
||||
CoreError::JsonRpc(corepc_jsonrpc::error::Error::Rpc(e)) => {
|
||||
e.code == -32600 || e.code == 401 || e.code == -28
|
||||
}
|
||||
CoreError::JsonRpc(corepc_jsonrpc::error::Error::Transport(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn retry<F, T>(max_retries: usize, delay: Duration, mut f: F) -> Result<T>
|
||||
where
|
||||
F: FnMut() -> Result<T>,
|
||||
{
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..=max_retries {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Retrying to connect to Bitcoin Core (attempt {}/{})",
|
||||
attempt, max_retries
|
||||
);
|
||||
sleep(delay);
|
||||
}
|
||||
|
||||
match f() {
|
||||
Ok(value) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Successfully connected to Bitcoin Core after {} retries",
|
||||
attempt
|
||||
);
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(e) => {
|
||||
if attempt == 0 {
|
||||
info!("Could not connect to Bitcoin Core, retrying: {}", e);
|
||||
}
|
||||
last_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let err = last_error.unwrap();
|
||||
info!(
|
||||
"Failed to connect to Bitcoin Core after {} attempts",
|
||||
max_retries + 1
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
|
||||
fn call_with_retry<F, T>(&self, f: F) -> Result<T, CoreError>
|
||||
where
|
||||
F: Fn(&CoreClient) -> Result<T, CoreError>,
|
||||
{
|
||||
for attempt in 0..=self.max_retries {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Trying to reconnect to Bitcoin Core (attempt {}/{})",
|
||||
attempt, self.max_retries
|
||||
);
|
||||
self.recreate().ok();
|
||||
sleep(self.retry_delay);
|
||||
}
|
||||
|
||||
match f(&self.client.read()) {
|
||||
Ok(value) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Successfully reconnected to Bitcoin Core after {} attempts",
|
||||
attempt
|
||||
);
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(e) if Self::is_retriable(&e) => {
|
||||
if attempt == 0 {
|
||||
info!("Lost connection to Bitcoin Core, reconnecting...");
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Could not reconnect to Bitcoin Core after {} attempts",
|
||||
self.max_retries + 1
|
||||
);
|
||||
Err(CoreError::JsonRpc(corepc_jsonrpc::error::Error::Rpc(
|
||||
corepc_jsonrpc::error::RpcError {
|
||||
code: -1,
|
||||
message: "Max retries exceeded".to_string(),
|
||||
data: None,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
// --- Wrapped methods returning shared types ---
|
||||
|
||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_blockchain_info())?;
|
||||
Ok(BlockchainInfo {
|
||||
headers: r.headers as u64,
|
||||
blocks: r.blocks as u64,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block(&self, hash: &bitcoin::BlockHash) -> Result<bitcoin::Block> {
|
||||
Ok(self.call_with_retry(|c| c.get_block(*hash))?)
|
||||
}
|
||||
|
||||
pub fn get_block_count(&self) -> Result<u64> {
|
||||
let r = self.call_with_retry(|c| c.get_block_count())?;
|
||||
Ok(r.0)
|
||||
}
|
||||
|
||||
pub fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash> {
|
||||
let r = self.call_with_retry(|c| c.get_block_hash(height))?;
|
||||
Ok(r.block_hash()?)
|
||||
}
|
||||
|
||||
/// Batched canonical height → block hash lookup over the inclusive
|
||||
/// range `start..=end`. Internally splits into JSON-RPC batches of
|
||||
/// `BATCH_CHUNK` requests so a 1M-block reindex doesn't try to push
|
||||
/// a 50 MB request body or hold every response in memory at once.
|
||||
///
|
||||
/// Returns hashes in canonical order (`start`, `start+1`, …, `end`).
|
||||
pub fn get_block_hashes_range(&self, start: u64, end: u64) -> Result<Vec<bitcoin::BlockHash>> {
|
||||
if end < start {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let total = (end - start + 1) as usize;
|
||||
let mut hashes = Vec::with_capacity(total);
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start <= end {
|
||||
let chunk_end = (chunk_start + BATCH_CHUNK as u64 - 1).min(end);
|
||||
self.batch_get_block_hashes(chunk_start, chunk_end, &mut hashes)?;
|
||||
chunk_start = chunk_end + 1;
|
||||
}
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
fn batch_get_block_hashes(
|
||||
&self,
|
||||
start: u64,
|
||||
end: u64,
|
||||
out: &mut Vec<bitcoin::BlockHash>,
|
||||
) -> Result<()> {
|
||||
let params: Vec<Box<RawValue>> = (start..=end)
|
||||
.map(|h| {
|
||||
RawValue::from_string(format!("[{h}]")).map_err(|e| Error::Parse(e.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let client = self.client.read();
|
||||
let requests: Vec<corepc_jsonrpc::Request> = params
|
||||
.iter()
|
||||
.map(|p| client.jsonrpc().build_request("getblockhash", Some(p)))
|
||||
.collect();
|
||||
|
||||
let responses = client
|
||||
.jsonrpc()
|
||||
.send_batch(&requests)
|
||||
.map_err(|e| Error::Parse(format!("getblockhash batch failed: {e}")))?;
|
||||
|
||||
for response in responses {
|
||||
let response = response.ok_or(Error::Internal("Missing response in JSON-RPC batch"))?;
|
||||
let hex: String = response
|
||||
.result()
|
||||
.map_err(|e| Error::Parse(format!("getblockhash batch result: {e}")))?;
|
||||
out.push(
|
||||
hex.parse::<bitcoin::BlockHash>()
|
||||
.map_err(|e| Error::Parse(format!("invalid block hash hex: {e}")))?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_block_header(&self, hash: &bitcoin::BlockHash) -> Result<bitcoin::block::Header> {
|
||||
let r = self.call_with_retry(|c| c.get_block_header(hash))?;
|
||||
r.block_header()
|
||||
.map_err(|_| CoreError::UnexpectedStructure.into())
|
||||
}
|
||||
|
||||
pub fn get_block_info(&self, hash: &bitcoin::BlockHash) -> Result<BlockInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_block_verbose_one(*hash))?;
|
||||
Ok(BlockInfo {
|
||||
height: r.height as usize,
|
||||
confirmations: r.confirmations,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block_header_info(&self, hash: &bitcoin::BlockHash) -> Result<BlockHeaderInfo> {
|
||||
let r = self.call_with_retry(|c| c.get_block_header_verbose(hash))?;
|
||||
let previous_block_hash = r
|
||||
.previous_block_hash
|
||||
.map(|s| s.parse::<bitcoin::BlockHash>())
|
||||
.transpose()
|
||||
.map_err(|_| corepc_client::client_sync::Error::UnexpectedStructure)?;
|
||||
Ok(BlockHeaderInfo {
|
||||
height: r.height as usize,
|
||||
confirmations: r.confirmations,
|
||||
previous_block_hash,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_tx_out(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
vout: u32,
|
||||
include_mempool: Option<bool>,
|
||||
) -> Result<Option<TxOutInfo>> {
|
||||
// corepc's typed get_tx_out doesn't support include_mempool, so use raw call
|
||||
let r: Option<TxOutResponse> = self.call_with_retry(|c| {
|
||||
let mut args = vec![
|
||||
serde_json::to_value(txid).map_err(CoreError::from)?,
|
||||
serde_json::to_value(vout).map_err(CoreError::from)?,
|
||||
];
|
||||
if let Some(mempool) = include_mempool {
|
||||
args.push(serde_json::to_value(mempool).map_err(CoreError::from)?);
|
||||
}
|
||||
c.call("gettxout", &args)
|
||||
})?;
|
||||
|
||||
match r {
|
||||
Some(r) => {
|
||||
let script_pub_key = bitcoin::ScriptBuf::from_hex(&r.script_pub_key.hex)
|
||||
.map_err(|_| corepc_client::client_sync::Error::UnexpectedStructure)?;
|
||||
let sats = (r.value * 100_000_000.0).round() as u64;
|
||||
Ok(Some(TxOutInfo {
|
||||
coinbase: r.coinbase,
|
||||
value: Sats::from(sats),
|
||||
script_pub_key,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<bitcoin::Txid>> {
|
||||
let r = self.call_with_retry(|c| c.get_raw_mempool())?;
|
||||
r.0.iter()
|
||||
.map(|s| {
|
||||
s.parse::<bitcoin::Txid>()
|
||||
.map_err(|_| corepc_client::client_sync::Error::UnexpectedStructure.into())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<(bitcoin::Txid, RawMempoolEntry)>> {
|
||||
let r = self.call_with_retry(|c| c.get_raw_mempool_verbose())?;
|
||||
r.0.into_iter()
|
||||
.map(|(txid_str, entry)| {
|
||||
let txid = txid_str
|
||||
.parse::<bitcoin::Txid>()
|
||||
.map_err(|_| corepc_client::client_sync::Error::UnexpectedStructure)?;
|
||||
let depends = entry
|
||||
.depends
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.parse::<bitcoin::Txid>()
|
||||
.map_err(|_| corepc_client::client_sync::Error::UnexpectedStructure)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok((
|
||||
txid,
|
||||
RawMempoolEntry {
|
||||
vsize: entry.vsize as u64,
|
||||
weight: entry.weight as u64,
|
||||
base_fee_sats: (entry.fees.base * 100_000_000.0).round() as u64,
|
||||
ancestor_count: entry.ancestor_count as u64,
|
||||
ancestor_size: entry.ancestor_size as u64,
|
||||
ancestor_fee_sats: (entry.fees.ancestor * 100_000_000.0).round() as u64,
|
||||
depends,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction_hex(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
block_hash: Option<&bitcoin::BlockHash>,
|
||||
) -> Result<String> {
|
||||
// corepc's get_raw_transaction doesn't support block_hash param, use raw call
|
||||
let r: String = self.call_with_retry(|c| {
|
||||
let mut args: Vec<serde_json::Value> = vec![
|
||||
serde_json::to_value(txid).map_err(CoreError::from)?,
|
||||
serde_json::Value::Bool(false),
|
||||
];
|
||||
if let Some(bh) = block_hash {
|
||||
args.push(serde_json::to_value(bh).map_err(CoreError::from)?);
|
||||
}
|
||||
c.call("getrawtransaction", &args)
|
||||
})?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<bitcoin::Txid> {
|
||||
let hex = hex.to_string();
|
||||
Ok(self.call_with_retry(|c| {
|
||||
let args = [serde_json::Value::String(hex.clone())];
|
||||
c.call("sendrawtransaction", &args)
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Transactions Bitcoin Core would include in the next block it would
|
||||
/// mine. Core requires the `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
let args = [serde_json::json!({ "rules": ["segwit"] })];
|
||||
let r: GetBlockTemplateResponse =
|
||||
self.call_with_retry(|c| c.call("getblocktemplate", &args))?;
|
||||
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
// Local deserialization structs for raw RPC responses
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TxOutResponse {
|
||||
coinbase: bool,
|
||||
value: f64,
|
||||
#[serde(rename = "scriptPubKey")]
|
||||
script_pub_key: TxOutScriptPubKey,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TxOutScriptPubKey {
|
||||
hex: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetBlockTemplateResponse {
|
||||
transactions: Vec<GetBlockTemplateTx>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetBlockTemplateTx {
|
||||
txid: bitcoin::Txid,
|
||||
fee: u64,
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bitcoin::ScriptBuf;
|
||||
use brk_types::{Sats, Txid};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockchainInfo {
|
||||
pub headers: u64,
|
||||
pub blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockInfo {
|
||||
pub height: usize,
|
||||
pub confirmations: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockHeaderInfo {
|
||||
pub height: usize,
|
||||
pub confirmations: i64,
|
||||
pub previous_block_hash: Option<bitcoin::BlockHash>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxOutInfo {
|
||||
pub coinbase: bool,
|
||||
pub value: Sats,
|
||||
pub script_pub_key: ScriptBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockTemplateTx {
|
||||
pub txid: Txid,
|
||||
pub fee: Sats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawMempoolEntry {
|
||||
pub vsize: u64,
|
||||
pub weight: u64,
|
||||
pub base_fee_sats: u64,
|
||||
pub ancestor_count: u64,
|
||||
pub ancestor_size: u64,
|
||||
pub ancestor_fee_sats: u64,
|
||||
pub depends: Vec<bitcoin::Txid>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Auth {
|
||||
None,
|
||||
UserPass(String, String),
|
||||
CookieFile(PathBuf),
|
||||
}
|
||||
|
||||
#[cfg(feature = "bitcoincore-rpc")]
|
||||
pub mod bitcoincore;
|
||||
|
||||
#[cfg(feature = "corepc")]
|
||||
pub mod corepc;
|
||||
|
||||
// Default ClientInner: prefer bitcoincore-rpc when both are enabled
|
||||
#[cfg(feature = "bitcoincore-rpc")]
|
||||
pub use bitcoincore::ClientInner;
|
||||
|
||||
#[cfg(all(feature = "corepc", not(feature = "bitcoincore-rpc")))]
|
||||
pub use corepc::ClientInner;
|
||||
|
||||
#[cfg(not(any(feature = "bitcoincore-rpc", feature = "corepc")))]
|
||||
compile_error!("brk_rpc requires either the `bitcoincore-rpc` or `corepc` feature");
|
||||
198
crates/brk_rpc/src/client.rs
Normal file
198
crates/brk_rpc/src/client.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use brk_error::{Error, Result};
|
||||
use corepc_jsonrpc::{
|
||||
Client as JsonRpcClient, Request, error::Error as JsonRpcError, simple_http,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use tracing::info;
|
||||
|
||||
use crate::Auth;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ClientInner {
|
||||
url: String,
|
||||
auth: Auth,
|
||||
client: RwLock<JsonRpcClient>,
|
||||
max_retries: usize,
|
||||
retry_delay: Duration,
|
||||
}
|
||||
|
||||
impl ClientInner {
|
||||
pub(crate) fn new(
|
||||
url: &str,
|
||||
auth: Auth,
|
||||
max_retries: usize,
|
||||
retry_delay: Duration,
|
||||
) -> Result<Self> {
|
||||
let client = Self::create_client(url, &auth)?;
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
auth,
|
||||
client: RwLock::new(client),
|
||||
max_retries,
|
||||
retry_delay,
|
||||
})
|
||||
}
|
||||
|
||||
/// Builds a `jsonrpc::Client` using the `simple_http` transport, which
|
||||
/// keeps a single pooled TCP socket with reconnect-on-failure. The
|
||||
/// upstream `corepc-client` hard-wires `bitreq_http` (one TCP connect
|
||||
/// per request), which collapses under concurrent load.
|
||||
fn create_client(url: &str, auth: &Auth) -> Result<JsonRpcClient> {
|
||||
let builder = simple_http::Builder::new()
|
||||
.url(url)
|
||||
.map_err(|e| Error::Parse(format!("bad rpc url: {e}")))?
|
||||
.timeout(Duration::from_secs(60));
|
||||
let builder = match auth {
|
||||
Auth::None => builder,
|
||||
Auth::UserPass(u, p) => builder.auth(u.clone(), Some(p.clone())),
|
||||
Auth::CookieFile(path) => {
|
||||
let cookie = std::fs::read_to_string(path)?;
|
||||
builder.cookie_auth(cookie.trim())
|
||||
}
|
||||
};
|
||||
Ok(JsonRpcClient::with_transport(builder.build()))
|
||||
}
|
||||
|
||||
fn recreate(&self) -> Result<()> {
|
||||
*self.client.write() = Self::create_client(&self.url, &self.auth)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_retriable(error: &JsonRpcError) -> bool {
|
||||
match error {
|
||||
JsonRpcError::Rpc(e) => e.code == -32600 || e.code == 401 || e.code == -28,
|
||||
JsonRpcError::Transport(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_with_retry<T>(&self, method: &str, args: &[Value]) -> Result<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let raw = serde_json::value::to_raw_value(args).map_err(Error::from)?;
|
||||
|
||||
for attempt in 0..=self.max_retries {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Trying to reconnect to Bitcoin Core (attempt {}/{})",
|
||||
attempt, self.max_retries
|
||||
);
|
||||
self.recreate().ok();
|
||||
sleep(self.retry_delay);
|
||||
}
|
||||
|
||||
match self.client.read().call::<T>(method, Some(&raw)) {
|
||||
Ok(value) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
"Successfully reconnected to Bitcoin Core after {} attempts",
|
||||
attempt
|
||||
);
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(e) if Self::is_retriable(&e) => {
|
||||
if attempt == 0 {
|
||||
info!("Lost connection to Bitcoin Core, reconnecting...");
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Could not reconnect to Bitcoin Core after {} attempts",
|
||||
self.max_retries + 1
|
||||
);
|
||||
Err(JsonRpcError::Rpc(corepc_jsonrpc::error::RpcError {
|
||||
code: -1,
|
||||
message: "Max retries exceeded".to_string(),
|
||||
data: None,
|
||||
})
|
||||
.into())
|
||||
}
|
||||
|
||||
pub(crate) fn call_once<T>(&self, method: &str, args: &[Value]) -> Result<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let raw = serde_json::value::to_raw_value(args).map_err(Error::from)?;
|
||||
Ok(self.client.read().call::<T>(method, Some(&raw))?)
|
||||
}
|
||||
|
||||
/// Send a batch of calls sharing `method`, one set of args per request.
|
||||
/// No retry: the caller decides batch sizing and failure semantics.
|
||||
pub(crate) fn call_batch<T>(
|
||||
&self,
|
||||
method: &str,
|
||||
batch_args: impl IntoIterator<Item = Vec<Value>>,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let params: Vec<Box<RawValue>> = batch_args
|
||||
.into_iter()
|
||||
.map(|args| serde_json::value::to_raw_value(&args).map_err(Error::from))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let client = self.client.read();
|
||||
let requests: Vec<Request> = params
|
||||
.iter()
|
||||
.map(|p| client.build_request(method, Some(p)))
|
||||
.collect();
|
||||
|
||||
let responses = client
|
||||
.send_batch(&requests)
|
||||
.map_err(|e| Error::Parse(format!("batch {method} failed: {e}")))?;
|
||||
|
||||
responses
|
||||
.into_iter()
|
||||
.map(|resp| {
|
||||
let resp = resp.ok_or(Error::Internal("Missing response in JSON-RPC batch"))?;
|
||||
resp.result::<T>()
|
||||
.map_err(|e| Error::Parse(format!("batch {method} result: {e}")))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Like `call_batch` but reports per-request success/failure independently,
|
||||
/// so one bad item doesn't nuke an otherwise-healthy chunk. The outer
|
||||
/// `Result` still fails if the HTTP round-trip itself fails.
|
||||
pub(crate) fn call_batch_per_item<T>(
|
||||
&self,
|
||||
method: &str,
|
||||
batch_args: impl IntoIterator<Item = Vec<Value>>,
|
||||
) -> Result<Vec<Result<T>>>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let params: Vec<Box<RawValue>> = batch_args
|
||||
.into_iter()
|
||||
.map(|args| serde_json::value::to_raw_value(&args).map_err(Error::from))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let client = self.client.read();
|
||||
let requests: Vec<Request> = params
|
||||
.iter()
|
||||
.map(|p| client.build_request(method, Some(p)))
|
||||
.collect();
|
||||
|
||||
let responses = client
|
||||
.send_batch(&requests)
|
||||
.map_err(|e| Error::Parse(format!("batch {method} failed: {e}")))?;
|
||||
|
||||
Ok(responses
|
||||
.into_iter()
|
||||
.map(|resp| {
|
||||
let resp = resp.ok_or(Error::Internal("Missing response in JSON-RPC batch"))?;
|
||||
resp.result::<T>()
|
||||
.map_err(|e| Error::Parse(format!("batch {method} result: {e}")))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,66 @@
|
||||
use std::{
|
||||
env, mem,
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bitcoin::consensus::encode;
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{BlockHash, Height, MempoolEntryInfo, Sats, Txid, Vout};
|
||||
use bitcoin::ScriptBuf;
|
||||
use brk_error::Result;
|
||||
use brk_types::{BlockHash, Hex, Sats, Txid};
|
||||
|
||||
pub mod backend;
|
||||
mod client;
|
||||
mod methods;
|
||||
|
||||
pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, TxOutInfo};
|
||||
use client::ClientInner;
|
||||
|
||||
use backend::ClientInner;
|
||||
use tracing::{debug, info};
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockchainInfo {
|
||||
pub headers: u64,
|
||||
pub blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockInfo {
|
||||
pub height: usize,
|
||||
pub confirmations: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockHeaderInfo {
|
||||
pub height: usize,
|
||||
pub confirmations: i64,
|
||||
pub previous_block_hash: Option<BlockHash>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TxOutInfo {
|
||||
pub coinbase: bool,
|
||||
pub value: Sats,
|
||||
pub script_pub_key: ScriptBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlockTemplateTx {
|
||||
pub txid: Txid,
|
||||
pub fee: Sats,
|
||||
}
|
||||
|
||||
/// A transaction fetched from Core alongside the exact hex bytes Core
|
||||
/// returned, so downstream code can re-emit the raw tx without re-
|
||||
/// serializing (which could diverge on segwit flag encoding, etc.).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawTx {
|
||||
pub tx: bitcoin::Transaction,
|
||||
pub hex: Hex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Auth {
|
||||
None,
|
||||
UserPass(String, String),
|
||||
CookieFile(PathBuf),
|
||||
}
|
||||
|
||||
///
|
||||
/// Bitcoin Core RPC Client
|
||||
@@ -23,7 +68,7 @@ use tracing::{debug, info};
|
||||
/// Thread safe and free to clone
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client(Arc<ClientInner>);
|
||||
pub struct Client(pub(crate) Arc<ClientInner>);
|
||||
|
||||
impl Client {
|
||||
pub fn new(url: &str, auth: Auth) -> Result<Self> {
|
||||
@@ -44,243 +89,6 @@ impl Client {
|
||||
)?)))
|
||||
}
|
||||
|
||||
/// Returns a data structure containing various state info regarding
|
||||
/// blockchain processing.
|
||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
||||
self.0.get_blockchain_info()
|
||||
}
|
||||
|
||||
pub fn get_block<'a, H>(&self, hash: &'a H) -> Result<bitcoin::Block>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
self.0.get_block(hash.into())
|
||||
}
|
||||
|
||||
/// Returns the numbers of block in the longest chain.
|
||||
pub fn get_block_count(&self) -> Result<u64> {
|
||||
self.0.get_block_count()
|
||||
}
|
||||
|
||||
/// Returns the numbers of block in the longest chain.
|
||||
pub fn get_last_height(&self) -> Result<Height> {
|
||||
self.0.get_block_count().map(Height::from)
|
||||
}
|
||||
|
||||
/// Get block hash at a given height
|
||||
pub fn get_block_hash<H>(&self, height: H) -> Result<BlockHash>
|
||||
where
|
||||
H: Into<u64> + Copy,
|
||||
{
|
||||
self.0.get_block_hash(height.into()).map(BlockHash::from)
|
||||
}
|
||||
|
||||
/// Get every canonical block hash for the inclusive height range
|
||||
/// `start..=end` in a single JSON-RPC batch request. Returns hashes
|
||||
/// in canonical order (`start`, `start+1`, …, `end`). Use this
|
||||
/// whenever resolving more than ~2 heights — one HTTP round-trip
|
||||
/// beats N sequential `get_block_hash` calls once the per-call
|
||||
/// overhead dominates.
|
||||
pub fn get_block_hashes_range<H1, H2>(&self, start: H1, end: H2) -> Result<Vec<BlockHash>>
|
||||
where
|
||||
H1: Into<u64>,
|
||||
H2: Into<u64>,
|
||||
{
|
||||
self.0
|
||||
.get_block_hashes_range(start.into(), end.into())
|
||||
.map(|v| v.into_iter().map(BlockHash::from).collect())
|
||||
}
|
||||
|
||||
pub fn get_block_header<'a, H>(&self, hash: &'a H) -> Result<bitcoin::block::Header>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
self.0.get_block_header(hash.into())
|
||||
}
|
||||
|
||||
pub fn get_block_info<'a, H>(&self, hash: &'a H) -> Result<BlockInfo>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
self.0.get_block_info(hash.into())
|
||||
}
|
||||
|
||||
pub fn get_block_header_info<'a, H>(&self, hash: &'a H) -> Result<BlockHeaderInfo>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
self.0.get_block_header_info(hash.into())
|
||||
}
|
||||
|
||||
pub fn get_transaction<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
block_hash: Option<&'a H>,
|
||||
) -> brk_error::Result<bitcoin::Transaction>
|
||||
where
|
||||
&'a T: Into<&'a bitcoin::Txid>,
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let tx = self.get_raw_transaction(txid, block_hash)?;
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub fn get_mempool_raw_tx(
|
||||
&self,
|
||||
txid: &Txid,
|
||||
) -> Result<(bitcoin::Transaction, String)> {
|
||||
let hex = self.get_raw_transaction_hex(txid, None as Option<&BlockHash>)?;
|
||||
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
|
||||
Ok((tx, hex))
|
||||
}
|
||||
|
||||
pub fn get_tx_out(
|
||||
&self,
|
||||
txid: &Txid,
|
||||
vout: Vout,
|
||||
include_mempool: Option<bool>,
|
||||
) -> Result<Option<TxOutInfo>> {
|
||||
self.0.get_tx_out(txid.into(), vout.into(), include_mempool)
|
||||
}
|
||||
|
||||
/// Get txids of all transactions in a memory pool
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
|
||||
self.0
|
||||
.get_raw_mempool()
|
||||
.map(|v| unsafe { mem::transmute(v) })
|
||||
}
|
||||
|
||||
/// Get all mempool entries with their fee data in a single RPC call
|
||||
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<MempoolEntryInfo>> {
|
||||
let result = self.0.get_raw_mempool_verbose()?;
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(
|
||||
|(txid, entry): (bitcoin::Txid, backend::RawMempoolEntry)| MempoolEntryInfo {
|
||||
txid: txid.into(),
|
||||
vsize: entry.vsize,
|
||||
weight: entry.weight,
|
||||
fee: Sats::from(entry.base_fee_sats),
|
||||
ancestor_count: entry.ancestor_count,
|
||||
ancestor_size: entry.ancestor_size,
|
||||
ancestor_fee: Sats::from(entry.ancestor_fee_sats),
|
||||
depends: entry.depends.into_iter().map(Txid::from).collect(),
|
||||
},
|
||||
)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
block_hash: Option<&'a H>,
|
||||
) -> brk_error::Result<bitcoin::Transaction>
|
||||
where
|
||||
&'a T: Into<&'a bitcoin::Txid>,
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hex = self.get_raw_transaction_hex(txid, block_hash)?;
|
||||
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction_hex<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
block_hash: Option<&'a H>,
|
||||
) -> Result<String>
|
||||
where
|
||||
&'a T: Into<&'a bitcoin::Txid>,
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
self.0
|
||||
.get_raw_transaction_hex(txid.into(), block_hash.map(|h| h.into()))
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<Txid> {
|
||||
self.0.send_raw_transaction(hex).map(Txid::from)
|
||||
}
|
||||
|
||||
/// Transactions (txid + fee) Bitcoin Core would include in the next
|
||||
/// block it would mine, via `getblocktemplate`.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
self.0.get_block_template_txs()
|
||||
}
|
||||
|
||||
/// Checks if a block is in the main chain (has positive confirmations)
|
||||
pub fn is_in_main_chain(&self, hash: &BlockHash) -> Result<bool> {
|
||||
let block_info = self.get_block_info(hash)?;
|
||||
Ok(block_info.confirmations > 0)
|
||||
}
|
||||
|
||||
pub fn get_closest_valid_height(&self, hash: BlockHash) -> Result<(Height, BlockHash)> {
|
||||
debug!("Get closest valid height...");
|
||||
|
||||
match self.get_block_header_info(&hash) {
|
||||
Ok(block_info) => {
|
||||
if self.is_in_main_chain(&hash)? {
|
||||
return Ok((block_info.height.into(), hash));
|
||||
}
|
||||
|
||||
let mut hash =
|
||||
block_info
|
||||
.previous_block_hash
|
||||
.map(BlockHash::from)
|
||||
.ok_or(Error::NotFound(
|
||||
"Genesis block has no previous block".into(),
|
||||
))?;
|
||||
|
||||
loop {
|
||||
if self.is_in_main_chain(&hash)? {
|
||||
let current_info = self.get_block_header_info(&hash)?;
|
||||
return Ok((current_info.height.into(), hash));
|
||||
}
|
||||
|
||||
let info = self.get_block_header_info(&hash)?;
|
||||
hash = info
|
||||
.previous_block_hash
|
||||
.map(BlockHash::from)
|
||||
.ok_or(Error::NotFound(
|
||||
"Reached genesis without finding main chain".into(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
Err(_) => Err(Error::NotFound("Block hash not found in blockchain".into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_synced_node(&self) -> Result<()> {
|
||||
let is_synced = || -> Result<bool> {
|
||||
let info = self.get_blockchain_info()?;
|
||||
Ok(info.headers == info.blocks)
|
||||
};
|
||||
|
||||
if !is_synced()? {
|
||||
info!("Waiting for node to sync...");
|
||||
while !is_synced()? {
|
||||
sleep(Duration::from_secs(1))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bitcoincore-rpc")]
|
||||
pub fn call<F, T>(&self, f: F) -> Result<T, bitcoincore_rpc::Error>
|
||||
where
|
||||
F: Fn(&bitcoincore_rpc::Client) -> Result<T, bitcoincore_rpc::Error>,
|
||||
{
|
||||
self.0.call_with_retry(f)
|
||||
}
|
||||
|
||||
#[cfg(feature = "bitcoincore-rpc")]
|
||||
pub fn call_once<F, T>(&self, f: F) -> Result<T, bitcoincore_rpc::Error>
|
||||
where
|
||||
F: Fn(&bitcoincore_rpc::Client) -> Result<T, bitcoincore_rpc::Error>,
|
||||
{
|
||||
self.0.call_once(f)
|
||||
}
|
||||
|
||||
pub fn default_url() -> &'static str {
|
||||
"http://localhost:8332"
|
||||
}
|
||||
|
||||
364
crates/brk_rpc/src/methods.rs
Normal file
364
crates/brk_rpc/src/methods.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use bitcoin::{consensus::encode, hex::FromHex};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{Bitcoin, BlockHash, Height, MempoolEntryInfo, Sats, Txid, Vout};
|
||||
use corepc_types::v30::{
|
||||
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
|
||||
GetBlockVerboseZero, GetBlockchainInfo, GetRawMempool, GetRawMempoolVerbose, GetTxOut,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, Client, RawTx, TxOutInfo};
|
||||
|
||||
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
||||
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
||||
/// spend too long on a single batch before yielding results.
|
||||
const BATCH_CHUNK: usize = 2000;
|
||||
|
||||
impl Client {
|
||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
||||
let r: GetBlockchainInfo = self.0.call_with_retry("getblockchaininfo", &[])?;
|
||||
Ok(BlockchainInfo {
|
||||
headers: r.headers as u64,
|
||||
blocks: r.blocks as u64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the numbers of block in the longest chain.
|
||||
pub fn get_block_count(&self) -> Result<u64> {
|
||||
let r: GetBlockCount = self.0.call_with_retry("getblockcount", &[])?;
|
||||
Ok(r.0)
|
||||
}
|
||||
|
||||
/// Returns the numbers of block in the longest chain.
|
||||
pub fn get_last_height(&self) -> Result<Height> {
|
||||
self.get_block_count().map(Height::from)
|
||||
}
|
||||
|
||||
pub fn get_block<'a, H>(&self, hash: &'a H) -> Result<bitcoin::Block>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hash: &bitcoin::BlockHash = hash.into();
|
||||
let r: GetBlockVerboseZero = self.0.call_with_retry(
|
||||
"getblock",
|
||||
&[serde_json::to_value(hash)?, Value::from(0u8)],
|
||||
)?;
|
||||
r.block()
|
||||
.map_err(|e| Error::Parse(format!("decode getblock: {e}")))
|
||||
}
|
||||
|
||||
pub fn get_block_info<'a, H>(&self, hash: &'a H) -> Result<BlockInfo>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hash: &bitcoin::BlockHash = hash.into();
|
||||
let r: GetBlockVerboseOne = self.0.call_with_retry(
|
||||
"getblock",
|
||||
&[serde_json::to_value(hash)?, Value::from(1u8)],
|
||||
)?;
|
||||
Ok(BlockInfo {
|
||||
height: r.height as usize,
|
||||
confirmations: r.confirmations,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_block_header<'a, H>(&self, hash: &'a H) -> Result<bitcoin::block::Header>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hash: &bitcoin::BlockHash = hash.into();
|
||||
let r: GetBlockHeader = self.0.call_with_retry(
|
||||
"getblockheader",
|
||||
&[serde_json::to_value(hash)?, Value::Bool(false)],
|
||||
)?;
|
||||
let bytes = Vec::from_hex(&r.0).map_err(|e| Error::Parse(format!("header hex: {e}")))?;
|
||||
bitcoin::consensus::deserialize::<bitcoin::block::Header>(&bytes).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_block_header_info<'a, H>(&self, hash: &'a H) -> Result<BlockHeaderInfo>
|
||||
where
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hash: &bitcoin::BlockHash = hash.into();
|
||||
let r: GetBlockHeaderVerbose = self
|
||||
.0
|
||||
.call_with_retry("getblockheader", &[serde_json::to_value(hash)?])?;
|
||||
let previous_block_hash = r
|
||||
.previous_block_hash
|
||||
.map(|s| Self::parse_block_hash(&s, "previousblockhash"))
|
||||
.transpose()?;
|
||||
Ok(BlockHeaderInfo {
|
||||
height: r.height as usize,
|
||||
confirmations: r.confirmations,
|
||||
previous_block_hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get block hash at a given height
|
||||
pub fn get_block_hash<H>(&self, height: H) -> Result<BlockHash>
|
||||
where
|
||||
H: Into<u64> + Copy,
|
||||
{
|
||||
let height: u64 = height.into();
|
||||
let r: GetBlockHash = self
|
||||
.0
|
||||
.call_with_retry("getblockhash", &[serde_json::to_value(height)?])?;
|
||||
Ok(BlockHash::from(r.block_hash()?))
|
||||
}
|
||||
|
||||
/// Get every canonical block hash for the inclusive height range
|
||||
/// `start..=end` in a single JSON-RPC batch request. Returns hashes
|
||||
/// in canonical order (`start`, `start+1`, …, `end`). Use this
|
||||
/// whenever resolving more than ~2 heights — one HTTP round-trip
|
||||
/// beats N sequential `get_block_hash` calls once the per-call
|
||||
/// overhead dominates.
|
||||
pub fn get_block_hashes_range<H1, H2>(&self, start: H1, end: H2) -> Result<Vec<BlockHash>>
|
||||
where
|
||||
H1: Into<u64>,
|
||||
H2: Into<u64>,
|
||||
{
|
||||
let start: u64 = start.into();
|
||||
let end: u64 = end.into();
|
||||
if end < start {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let total = (end - start + 1) as usize;
|
||||
let mut hashes = Vec::with_capacity(total);
|
||||
|
||||
let mut chunk_start = start;
|
||||
while chunk_start <= end {
|
||||
let chunk_end = (chunk_start + BATCH_CHUNK as u64 - 1).min(end);
|
||||
let args = (chunk_start..=chunk_end).map(|h| vec![Value::from(h)]);
|
||||
let chunk: Vec<String> = self.0.call_batch("getblockhash", args)?;
|
||||
for hex in chunk {
|
||||
hashes.push(Self::parse_block_hash(&hex, "getblockhash batch")?);
|
||||
}
|
||||
chunk_start = chunk_end + 1;
|
||||
}
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
pub fn get_tx_out(
|
||||
&self,
|
||||
txid: &Txid,
|
||||
vout: Vout,
|
||||
include_mempool: Option<bool>,
|
||||
) -> Result<Option<TxOutInfo>> {
|
||||
let txid: &bitcoin::Txid = txid.into();
|
||||
let mut args: Vec<Value> = vec![
|
||||
serde_json::to_value(txid)?,
|
||||
serde_json::to_value(u32::from(vout))?,
|
||||
];
|
||||
if let Some(mempool) = include_mempool {
|
||||
args.push(Value::Bool(mempool));
|
||||
}
|
||||
let r: Option<GetTxOut> = self.0.call_with_retry("gettxout", &args)?;
|
||||
match r {
|
||||
Some(r) => {
|
||||
let script_pub_key = bitcoin::ScriptBuf::from_hex(&r.script_pubkey.hex)
|
||||
.map_err(|e| Error::Parse(format!("script hex: {e}")))?;
|
||||
Ok(Some(TxOutInfo {
|
||||
coinbase: r.coinbase,
|
||||
value: Sats::from(Bitcoin::from(r.value)),
|
||||
script_pub_key,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get txids of all transactions in a memory pool
|
||||
pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
|
||||
let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?;
|
||||
r.0.iter()
|
||||
.map(|s| Self::parse_txid(s, "mempool txid"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all mempool entries with their fee data in a single RPC call
|
||||
pub fn get_raw_mempool_verbose(&self) -> Result<Vec<MempoolEntryInfo>> {
|
||||
let r: GetRawMempoolVerbose = self
|
||||
.0
|
||||
.call_with_retry("getrawmempool", &[Value::Bool(true)])?;
|
||||
r.0.into_iter()
|
||||
.map(|(txid_str, entry)| {
|
||||
let depends = entry
|
||||
.depends
|
||||
.iter()
|
||||
.map(|s| Self::parse_txid(s, "depends txid"))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(MempoolEntryInfo {
|
||||
txid: Self::parse_txid(&txid_str, "mempool txid")?,
|
||||
vsize: entry.vsize as u64,
|
||||
weight: entry.weight as u64,
|
||||
fee: Sats::from(Bitcoin::from(entry.fees.base)),
|
||||
ancestor_count: entry.ancestor_count as u64,
|
||||
ancestor_size: entry.ancestor_size as u64,
|
||||
ancestor_fee: Sats::from(Bitcoin::from(entry.fees.ancestor)),
|
||||
depends,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
block_hash: Option<&'a H>,
|
||||
) -> Result<bitcoin::Transaction>
|
||||
where
|
||||
&'a T: Into<&'a bitcoin::Txid>,
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let hex = self.get_raw_transaction_hex(txid, block_hash)?;
|
||||
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub fn get_raw_transaction_hex<'a, T, H>(
|
||||
&self,
|
||||
txid: &'a T,
|
||||
block_hash: Option<&'a H>,
|
||||
) -> Result<String>
|
||||
where
|
||||
&'a T: Into<&'a bitcoin::Txid>,
|
||||
&'a H: Into<&'a bitcoin::BlockHash>,
|
||||
{
|
||||
let txid: &bitcoin::Txid = txid.into();
|
||||
let mut args: Vec<Value> = vec![serde_json::to_value(txid)?, Value::Bool(false)];
|
||||
if let Some(bh) = block_hash {
|
||||
let bh: &bitcoin::BlockHash = bh.into();
|
||||
args.push(serde_json::to_value(bh)?);
|
||||
}
|
||||
self.0.call_with_retry("getrawtransaction", &args)
|
||||
}
|
||||
|
||||
pub fn get_mempool_raw_tx(&self, txid: &Txid) -> Result<RawTx> {
|
||||
let hex = self.get_raw_transaction_hex(txid, None as Option<&BlockHash>)?;
|
||||
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
|
||||
Ok(RawTx { tx, hex: hex.into() })
|
||||
}
|
||||
|
||||
/// Batched `getrawtransaction` over a slice of txids. Returns a map keyed
|
||||
/// by txid containing the deserialized tx and its raw hex. Individual
|
||||
/// failures (e.g. a tx that evicted between the listing and this call)
|
||||
/// are logged and dropped so a single bad entry doesn't kill the batch.
|
||||
///
|
||||
/// Chunked at `BATCH_CHUNK` requests per round-trip.
|
||||
pub fn get_raw_transactions(
|
||||
&self,
|
||||
txids: &[Txid],
|
||||
) -> Result<FxHashMap<Txid, RawTx>> {
|
||||
let mut out: FxHashMap<Txid, RawTx> =
|
||||
FxHashMap::with_capacity_and_hasher(txids.len(), Default::default());
|
||||
|
||||
for chunk in txids.chunks(BATCH_CHUNK) {
|
||||
let args = chunk.iter().map(|t| {
|
||||
let bt: &bitcoin::Txid = t.into();
|
||||
vec![
|
||||
serde_json::to_value(bt).unwrap_or(Value::Null),
|
||||
Value::Bool(false),
|
||||
]
|
||||
});
|
||||
let results: Vec<Result<String>> =
|
||||
self.0.call_batch_per_item("getrawtransaction", args)?;
|
||||
|
||||
for (txid, res) in chunk.iter().zip(results) {
|
||||
match res.and_then(|hex| {
|
||||
let tx = encode::deserialize_hex::<bitcoin::Transaction>(&hex)?;
|
||||
Ok::<_, Error>(RawTx { tx, hex: hex.into() })
|
||||
}) {
|
||||
Ok(raw) => {
|
||||
out.insert(txid.clone(), raw);
|
||||
}
|
||||
// Silenced: users without `-txindex` expect -5 for
|
||||
// every confirmed tx. Downgraded so the mempool
|
||||
// parent-fetch loop doesn't spam the log each cycle.
|
||||
Err(e) => debug!(txid = %txid, error = %e, "getrawtransaction batch: item failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn send_raw_transaction(&self, hex: &str) -> Result<Txid> {
|
||||
let txid: bitcoin::Txid = self
|
||||
.0
|
||||
.call_once("sendrawtransaction", &[Value::String(hex.to_string())])?;
|
||||
Ok(Txid::from(txid))
|
||||
}
|
||||
|
||||
/// Transactions (txid + fee) Bitcoin Core would include in the next
|
||||
/// block it would mine, via `getblocktemplate`. Core requires the
|
||||
/// `segwit` rule to be declared.
|
||||
pub fn get_block_template_txs(&self) -> Result<Vec<BlockTemplateTx>> {
|
||||
#[derive(Deserialize)]
|
||||
struct Response {
|
||||
transactions: Vec<Tx>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Tx {
|
||||
txid: bitcoin::Txid,
|
||||
fee: u64,
|
||||
}
|
||||
|
||||
let args = [serde_json::json!({ "rules": ["segwit"] })];
|
||||
let r: Response = self.0.call_with_retry("getblocktemplate", &args)?;
|
||||
Ok(r.transactions
|
||||
.into_iter()
|
||||
.map(|t| BlockTemplateTx {
|
||||
txid: Txid::from(t.txid),
|
||||
fee: Sats::from(t.fee),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_closest_valid_height(&self, hash: BlockHash) -> Result<(Height, BlockHash)> {
|
||||
debug!("Get closest valid height...");
|
||||
|
||||
let mut current = hash;
|
||||
loop {
|
||||
let info = self.get_block_header_info(¤t)?;
|
||||
if info.confirmations > 0 {
|
||||
return Ok((info.height.into(), current));
|
||||
}
|
||||
current = info.previous_block_hash.ok_or(Error::NotFound(
|
||||
"Reached genesis without finding main chain".into(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_synced_node(&self) -> Result<()> {
|
||||
let is_synced = || -> Result<bool> {
|
||||
let info = self.get_blockchain_info()?;
|
||||
Ok(info.headers == info.blocks)
|
||||
};
|
||||
|
||||
if !is_synced()? {
|
||||
info!("Waiting for node to sync...");
|
||||
while !is_synced()? {
|
||||
sleep(Duration::from_secs(1))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_txid(s: &str, label: &str) -> Result<Txid> {
|
||||
s.parse::<bitcoin::Txid>()
|
||||
.map(Txid::from)
|
||||
.map_err(|e| Error::Parse(format!("{label}: {e}")))
|
||||
}
|
||||
|
||||
fn parse_block_hash(s: &str, label: &str) -> Result<BlockHash> {
|
||||
s.parse::<bitcoin::BlockHash>()
|
||||
.map(BlockHash::from)
|
||||
.map_err(|e| Error::Parse(format!("{label}: {e}")))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user