#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![warn(missing_docs)]
#[allow(unused_imports)]
#[macro_use]
extern crate alloc;
use alloc::sync::Arc;
use bdk_core::collections::{HashMap, HashSet};
use bdk_core::{BlockId, CheckPoint};
use bitcoin::{Block, BlockHash, Transaction, Txid};
use bitcoincore_rpc::{bitcoincore_rpc_json, RpcApi};
use core::ops::Deref;
pub mod bip158;
pub use bitcoincore_rpc;
pub struct Emitter<C> {
client: C,
start_height: u32,
last_cp: CheckPoint,
last_block: Option<bitcoincore_rpc_json::GetBlockResult>,
mempool_snapshot: HashMap<Txid, Arc<Transaction>>,
}
pub const NO_EXPECTED_MEMPOOL_TXS: core::iter::Empty<Arc<Transaction>> = core::iter::empty();
impl<C> Emitter<C>
where
C: Deref,
C::Target: RpcApi,
{
pub fn new(
client: C,
last_cp: CheckPoint,
start_height: u32,
expected_mempool_txs: impl IntoIterator<Item = impl Into<Arc<Transaction>>>,
) -> Self {
Self {
client,
start_height,
last_cp,
last_block: None,
mempool_snapshot: expected_mempool_txs
.into_iter()
.map(|tx| {
let tx: Arc<Transaction> = tx.into();
(tx.compute_txid(), tx)
})
.collect(),
}
}
#[cfg(feature = "std")]
pub fn mempool(&mut self) -> Result<MempoolEvent, bitcoincore_rpc::Error> {
let sync_time = std::time::UNIX_EPOCH
.elapsed()
.expect("must get current time")
.as_secs();
self.mempool_at(sync_time)
}
pub fn mempool_at(&mut self, sync_time: u64) -> Result<MempoolEvent, bitcoincore_rpc::Error> {
let client = &*self.client;
let mut rpc_tip_height;
let mut rpc_tip_hash;
let mut rpc_mempool;
let mut rpc_mempool_txids;
loop {
rpc_tip_height = client.get_block_count()?;
rpc_tip_hash = client.get_block_hash(rpc_tip_height)?;
rpc_mempool = client.get_raw_mempool()?;
rpc_mempool_txids = rpc_mempool.iter().copied().collect::<HashSet<Txid>>();
let is_still_at_tip = rpc_tip_hash == client.get_block_hash(rpc_tip_height)?
&& rpc_tip_height == client.get_block_count()?;
if is_still_at_tip {
break;
}
}
let mut mempool_event = MempoolEvent {
update: rpc_mempool
.into_iter()
.filter_map(|txid| -> Option<Result<_, bitcoincore_rpc::Error>> {
let tx = match self.mempool_snapshot.get(&txid) {
Some(tx) => tx.clone(),
None => match client.get_raw_transaction(&txid, None) {
Ok(tx) => {
let tx = Arc::new(tx);
self.mempool_snapshot.insert(txid, tx.clone());
tx
}
Err(err) if err.is_not_found_error() => return None,
Err(err) => return Some(Err(err)),
},
};
Some(Ok((tx, sync_time)))
})
.collect::<Result<Vec<_>, _>>()?,
..Default::default()
};
let at_tip =
rpc_tip_height == self.last_cp.height() as u64 && rpc_tip_hash == self.last_cp.hash();
if at_tip {
mempool_event.evicted = self
.mempool_snapshot
.keys()
.filter(|&txid| !rpc_mempool_txids.contains(txid))
.map(|&txid| (txid, sync_time))
.collect();
self.mempool_snapshot = mempool_event
.update
.iter()
.map(|(tx, _)| (tx.compute_txid(), tx.clone()))
.collect();
} else {
self.mempool_snapshot.extend(
mempool_event
.update
.iter()
.map(|(tx, _)| (tx.compute_txid(), tx.clone())),
);
};
Ok(mempool_event)
}
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
if let Some((checkpoint, block)) = poll(self, move |hash, client| client.get_block(hash))? {
for tx in &block.txdata {
self.mempool_snapshot.remove(&tx.compute_txid());
}
return Ok(Some(BlockEvent { block, checkpoint }));
}
Ok(None)
}
}
#[derive(Debug, Default)]
pub struct MempoolEvent {
pub update: Vec<(Arc<Transaction>, u64)>,
pub evicted: Vec<(Txid, u64)>,
}
#[derive(Debug)]
pub struct BlockEvent<B> {
pub block: B,
pub checkpoint: CheckPoint,
}
impl<B> BlockEvent<B> {
pub fn block_height(&self) -> u32 {
self.checkpoint.height()
}
pub fn block_hash(&self) -> BlockHash {
self.checkpoint.hash()
}
pub fn connected_to(&self) -> BlockId {
match self.checkpoint.prev() {
Some(prev_cp) => prev_cp.block_id(),
None => self.checkpoint.block_id(),
}
}
}
enum PollResponse {
Block(bitcoincore_rpc_json::GetBlockResult),
NoMoreBlocks,
BlockNotInBestChain,
AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
AgreementPointNotFound(BlockHash),
}
fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
where
C: Deref,
C::Target: RpcApi,
{
let client = &*emitter.client;
if let Some(last_res) = &emitter.last_block {
let next_hash = if last_res.height < emitter.start_height as _ {
let next_hash = client.get_block_hash(emitter.start_height as _)?;
if client.get_block_hash(last_res.height as _)? != last_res.hash {
return Ok(PollResponse::BlockNotInBestChain);
}
next_hash
} else {
match last_res.nextblockhash {
None => return Ok(PollResponse::NoMoreBlocks),
Some(next_hash) => next_hash,
}
};
let res = client.get_block_info(&next_hash)?;
if res.confirmations < 0 {
return Ok(PollResponse::BlockNotInBestChain);
}
return Ok(PollResponse::Block(res));
}
for cp in emitter.last_cp.iter() {
let res = match client.get_block_info(&cp.hash()) {
Ok(res) if res.confirmations < 0 => continue,
Ok(res) => res,
Err(e) if e.is_not_found_error() => {
if cp.height() > 0 {
continue;
}
break;
}
Err(e) => return Err(e),
};
return Ok(PollResponse::AgreementFound(res, cp));
}
let genesis_hash = client.get_block_hash(0)?;
Ok(PollResponse::AgreementPointNotFound(genesis_hash))
}
fn poll<C, V, F>(
emitter: &mut Emitter<C>,
get_item: F,
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
where
C: Deref,
C::Target: RpcApi,
F: Fn(&BlockHash, &C::Target) -> Result<V, bitcoincore_rpc::Error>,
{
loop {
match poll_once(emitter)? {
PollResponse::Block(res) => {
let height = res.height as u32;
let hash = res.hash;
let item = get_item(&hash, &emitter.client)?;
let new_cp = emitter
.last_cp
.clone()
.push(BlockId { height, hash })
.expect("must push");
emitter.last_cp = new_cp.clone();
emitter.last_block = Some(res);
return Ok(Some((new_cp, item)));
}
PollResponse::NoMoreBlocks => {
emitter.last_block = None;
return Ok(None);
}
PollResponse::BlockNotInBestChain => {
emitter.last_block = None;
continue;
}
PollResponse::AgreementFound(res, cp) => {
emitter.last_cp = cp;
emitter.last_block = Some(res);
continue;
}
PollResponse::AgreementPointNotFound(genesis_hash) => {
emitter.last_cp = CheckPoint::new(BlockId {
height: 0,
hash: genesis_hash,
});
emitter.last_block = None;
continue;
}
}
}
}
pub trait BitcoindRpcErrorExt {
fn is_not_found_error(&self) -> bool;
}
impl BitcoindRpcErrorExt for bitcoincore_rpc::Error {
fn is_not_found_error(&self) -> bool {
if let bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(rpc_err)) = self
{
rpc_err.code == -5
} else {
false
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
use crate::{bitcoincore_rpc::RpcApi, Emitter, NO_EXPECTED_MEMPOOL_TXS};
use bdk_chain::local_chain::LocalChain;
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash};
use std::collections::HashSet;
#[test]
fn test_expected_mempool_txids_accumulate_and_remove() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let chain = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?).0;
let chain_tip = chain.tip();
let mut emitter = Emitter::new(
env.rpc_client(),
chain_tip.clone(),
1,
NO_EXPECTED_MEMPOOL_TXS,
);
env.mine_blocks(100, None)?;
while emitter.next_block()?.is_some() {}
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
let mut mempool_txids = HashSet::new();
for _ in 0..10 {
let sent_txid = env.send(&addr_to_track, Amount::from_sat(1_000))?;
mempool_txids.insert(sent_txid);
emitter.mempool()?;
env.mine_blocks(1, None)?;
for txid in &mempool_txids {
assert!(
emitter.mempool_snapshot.contains_key(txid),
"Expected txid {txid:?} missing"
);
}
}
while let Some(block_event) = emitter.next_block()? {
let confirmed_txids: HashSet<Txid> = block_event
.block
.txdata
.iter()
.map(|tx| tx.compute_txid())
.collect();
mempool_txids = mempool_txids
.difference(&confirmed_txids)
.copied()
.collect::<HashSet<_>>();
for txid in confirmed_txids {
assert!(
!emitter.mempool_snapshot.contains_key(&txid),
"Expected txid {txid:?} should have been removed"
);
}
for txid in &mempool_txids {
assert!(
emitter.mempool_snapshot.contains_key(txid),
"Expected txid {txid:?} missing"
);
}
}
assert!(emitter.mempool_snapshot.is_empty());
Ok(())
}
}