use alloc::sync::Arc;
use core::fmt;
use arc_swap::{ArcSwap, ArcSwapOption};
use bitcoin::consensus::encode::serialize;
use bitcoin::hashes::Hash as _;
use bitcoin::hex::{DisplayHex, FromHex as _};
use bitcoin::{Block, Transaction, Txid};
use bitcoin_rs_chain::TipSnapshot;
use bitcoin_rs_mempool::{Mempool, MempoolLimits};
use bitcoin_rs_primitives::{Hash256, Network};
use compact_str::CompactString;
use crossbeam_channel::{Receiver, Sender, unbounded};
use hashbrown::HashMap;
use parking_lot::{Mutex, RwLock};
const SERIALIZED_BLOCK_HEADER_LEN: usize = 80;
#[derive(Clone, Debug)]
pub struct BlockRecord {
pub hash: Hash256,
pub height: u32,
pub block_hex: String,
pub body_size: usize,
pub header_hex: String,
pub tx_count: usize,
pub time: u32,
}
pub trait BlockBodySource: Send + Sync {
fn block_body(&self, height: u32, hash: Hash256) -> Option<Vec<u8>>;
}
impl BlockRecord {
#[must_use]
pub fn from_block(height: u32, block: &Block) -> Self {
let block_bytes = serialize(block);
Self::from_block_bytes(height, block, &block_bytes)
}
#[must_use]
pub fn from_block_bytes(height: u32, block: &Block, block_bytes: &[u8]) -> Self {
let block_hash = block.block_hash();
let hash = Hash256::from_le_bytes(block_hash.as_byte_array());
let header_hex = header_hex_from_block_bytes(block, block_bytes);
let block_hex = block_bytes.to_lower_hex_string();
Self {
hash,
height,
block_hex,
body_size: block_bytes.len(),
header_hex,
tx_count: block.txdata.len(),
time: block.header.time,
}
}
#[must_use]
pub fn from_block_metadata(height: u32, block: &Block) -> Self {
let block_bytes = serialize(block);
Self::from_block_metadata_bytes(height, block, &block_bytes)
}
#[must_use]
pub fn from_block_metadata_bytes(height: u32, block: &Block, block_bytes: &[u8]) -> Self {
let block_hash = block.block_hash();
let hash = Hash256::from_le_bytes(block_hash.as_byte_array());
let header_hex = header_hex_from_block_bytes(block, block_bytes);
Self {
hash,
height,
block_hex: String::new(),
body_size: block_bytes.len(),
header_hex,
tx_count: block.txdata.len(),
time: block.header.time,
}
}
#[must_use]
pub fn synthetic(height: u32, hash: Hash256) -> Self {
Self {
hash,
height,
block_hex: String::new(),
body_size: 0,
header_hex: String::new(),
tx_count: 0,
time: 0,
}
}
}
fn header_hex_from_block_bytes(block: &Block, block_bytes: &[u8]) -> String {
block_bytes.get(..SERIALIZED_BLOCK_HEADER_LEN).map_or_else(
|| serialize(&block.header).to_lower_hex_string(),
DisplayHex::to_lower_hex_string,
)
}
#[derive(Clone, Debug, Default)]
pub struct NetworkState {
pub connection_count: u64,
pub bytes_recv: u64,
pub bytes_sent: u64,
pub timestamp: u64,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct PruneStatus {
pub pruned: bool,
pub pruneheight: Option<u32>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct PruneResult {
pub requested_height: u32,
pub pruneheight: u32,
pub block_rows_removed: u64,
pub undo_rows_removed: u64,
pub bytes_freed: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ZmqNotification {
pub notification_type: CompactString,
pub address: String,
pub hwm: u32,
}
impl ZmqNotification {
#[must_use]
pub fn new(
notification_type: impl Into<CompactString>,
address: impl Into<String>,
hwm: u32,
) -> Self {
Self {
notification_type: notification_type.into(),
address: address.into(),
hwm,
}
}
}
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum PruneServiceError {
#[error("{0}")]
Failed(String),
}
impl PruneServiceError {
#[must_use]
pub fn failed(message: impl Into<String>) -> Self {
Self::Failed(message.into())
}
}
pub trait PruneService: Send + Sync {
fn prune_to_height(&self, requested_height: u32) -> Result<PruneResult, PruneServiceError>;
fn status(&self) -> PruneStatus;
}
#[derive(Debug, Default)]
struct NoopFilterIndex;
impl bitcoin_rs_filters::FilterIndexLike for NoopFilterIndex {
fn put_filter(
&self,
_block_hash: bitcoin_rs_primitives::Hash256,
_prev_header: bitcoin_rs_primitives::Hash256,
_filter_bytes: &[u8],
) -> Result<bitcoin_rs_primitives::Hash256, bitcoin_rs_filters::FilterIndexError> {
Ok(bitcoin_rs_primitives::Hash256::default())
}
fn filter_header(
&self,
_block_hash: bitcoin_rs_primitives::Hash256,
) -> Result<Option<bitcoin_rs_primitives::Hash256>, bitcoin_rs_filters::FilterIndexError> {
Ok(None)
}
fn filter(
&self,
_block_hash: bitcoin_rs_primitives::Hash256,
) -> Result<Option<Vec<u8>>, bitcoin_rs_filters::FilterIndexError> {
Ok(None)
}
}
fn noop_filter_index() -> Arc<Box<dyn bitcoin_rs_filters::FilterIndexLike>> {
let filter_index: Box<dyn bitcoin_rs_filters::FilterIndexLike> = Box::new(NoopFilterIndex);
Arc::new(filter_index)
}
pub struct Context {
pub chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
pub applied_tip: Arc<ArcSwapOption<TipSnapshot>>,
pub mempool: Arc<RwLock<Mempool>>,
pub blocks: Arc<RwLock<Vec<BlockRecord>>>,
pub transactions: Arc<RwLock<HashMap<Txid, Transaction>>>,
pub utxo: Arc<bitcoin_rs_utxo::UtxoSet>,
pub coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
pub filter_index: Arc<Box<dyn bitcoin_rs_filters::FilterIndexLike>>,
pub prune_service: Option<Arc<dyn PruneService>>,
pub indexer: Option<Arc<Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>>,
pub network: Arc<RwLock<NetworkState>>,
pub chain_network: Network,
pub peers: Arc<RwLock<Vec<bitcoin_rs_p2p::PeerInfo>>>,
pub block_tree: Arc<parking_lot::RwLock<bitcoin_rs_chain::BlockTree>>,
pub block_body_source: Option<Arc<dyn BlockBodySource>>,
pub mining_template_id: Arc<ArcSwap<CompactString>>,
pub mining_notifications: Receiver<()>,
pub inbound_blocks_sender: Option<crossbeam_channel::Sender<bitcoin_rs_p2p::InboundBlock>>,
pub p2p_outbound_sender: Option<crossbeam_channel::Sender<std::net::SocketAddr>>,
pub banned: Arc<parking_lot::RwLock<Vec<bitcoin_rs_p2p::BannedSubnet>>>,
pub added_nodes: Arc<parking_lot::RwLock<Vec<std::net::SocketAddr>>>,
pub zmq_notifications: Arc<[ZmqNotification]>,
mining_sender: Sender<()>,
}
#[allow(clippy::non_send_fields_in_send_ty)]
unsafe impl Send for Context {}
unsafe impl Sync for Context {}
impl fmt::Debug for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context").finish_non_exhaustive()
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
#[must_use]
#[allow(clippy::arc_with_non_send_sync)]
pub fn new() -> Self {
let (mining_sender, mining_notifications) = unbounded();
let coin_stats_listener = bitcoin_rs_coinstats::CoinStatsListener::new(
bitcoin_rs_coinstats::CoinStats::default(),
);
let mut utxo = bitcoin_rs_utxo::UtxoSet::new();
utxo.set_listener(Box::new(coin_stats_listener.clone()));
let coin_stats = Arc::new(coin_stats_listener);
Self {
chain_tip: Arc::new(ArcSwapOption::empty()),
applied_tip: Arc::new(ArcSwapOption::empty()),
mempool: Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
blocks: Arc::new(RwLock::new(Vec::new())),
transactions: Arc::new(RwLock::new(HashMap::new())),
utxo: Arc::new(utxo),
coin_stats,
filter_index: noop_filter_index(),
indexer: None,
prune_service: None,
network: Arc::new(RwLock::new(NetworkState::default())),
chain_network: Network::Mainnet,
peers: Arc::new(RwLock::new(Vec::new())),
block_tree: Arc::new(parking_lot::RwLock::new(bitcoin_rs_chain::BlockTree::new())),
block_body_source: None,
mining_template_id: Arc::new(ArcSwap::from_pointee(CompactString::new("0"))),
mining_notifications,
inbound_blocks_sender: None,
p2p_outbound_sender: None,
banned: Arc::new(parking_lot::RwLock::new(Vec::new())),
added_nodes: Arc::new(parking_lot::RwLock::new(Vec::new())),
zmq_notifications: Arc::from(Vec::<ZmqNotification>::new()),
mining_sender,
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn from_handles(
chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
applied_tip: Arc<ArcSwapOption<TipSnapshot>>,
mempool: Arc<RwLock<Mempool>>,
blocks: Arc<RwLock<Vec<BlockRecord>>>,
transactions: Arc<RwLock<HashMap<Txid, Transaction>>>,
utxo: Arc<bitcoin_rs_utxo::UtxoSet>,
coin_stats: Arc<bitcoin_rs_coinstats::CoinStatsListener>,
filter_index: Arc<Box<dyn bitcoin_rs_filters::FilterIndexLike>>,
network: Arc<RwLock<NetworkState>>,
mining_template_id: Arc<ArcSwap<CompactString>>,
peers: Arc<RwLock<Vec<bitcoin_rs_p2p::PeerInfo>>>,
block_tree: Arc<parking_lot::RwLock<bitcoin_rs_chain::BlockTree>>,
chain_network: Network,
inbound_blocks_sender: Option<crossbeam_channel::Sender<bitcoin_rs_p2p::InboundBlock>>,
p2p_outbound_sender: Option<crossbeam_channel::Sender<std::net::SocketAddr>>,
banned: Arc<parking_lot::RwLock<Vec<bitcoin_rs_p2p::BannedSubnet>>>,
added_nodes: Arc<parking_lot::RwLock<Vec<std::net::SocketAddr>>>,
indexer: Option<Arc<Mutex<Box<dyn bitcoin_rs_index::IndexerLike>>>>,
) -> Self {
let (mining_sender, mining_notifications) = unbounded();
Self {
chain_tip,
applied_tip,
mempool,
blocks,
transactions,
utxo,
coin_stats,
filter_index,
indexer,
network,
chain_network,
peers,
block_tree,
block_body_source: None,
mining_template_id,
mining_notifications,
inbound_blocks_sender,
p2p_outbound_sender,
banned,
added_nodes,
prune_service: None,
zmq_notifications: Arc::from(Vec::<ZmqNotification>::new()),
mining_sender,
}
}
#[must_use]
pub fn with_block_body_source(mut self, source: Arc<dyn BlockBodySource>) -> Self {
self.block_body_source = Some(source);
self
}
#[must_use]
pub fn with_prune_service(mut self, prune_service: Arc<dyn PruneService>) -> Self {
self.prune_service = Some(prune_service);
self
}
#[must_use]
pub fn with_zmq_notifications(mut self, notifications: Vec<ZmqNotification>) -> Self {
self.zmq_notifications = Arc::from(notifications);
self
}
#[must_use]
pub fn zmq_notifications(&self) -> &[ZmqNotification] {
self.zmq_notifications.as_ref()
}
#[must_use]
pub fn prune_status(&self) -> PruneStatus {
self.prune_service
.as_ref()
.map_or_else(PruneStatus::default, |service| service.status())
}
#[must_use]
pub fn difficulty_for_bits(&self, bits: bitcoin::CompactTarget) -> f64 {
let params = bitcoin::params::Params::new(bitcoin_network(self.chain_network));
let current_target = bitcoin::pow::Target::from_compact(bits);
if current_target == bitcoin::pow::Target::ZERO {
return 0.0;
}
target_to_f64(params.max_attainable_target) / target_to_f64(current_target)
}
pub fn set_chain_tip(&self, tip: TipSnapshot) {
self.mining_template_id
.store(Arc::new(CompactString::from(tip.hash.to_string_be())));
self.chain_tip.store(Some(Arc::new(tip)));
let _ignored = self.mining_sender.send(());
}
pub fn set_applied_tip(&self, tip: TipSnapshot) {
self.applied_tip.store(Some(Arc::new(tip)));
}
pub fn add_block(&self, record: BlockRecord) {
self.blocks.write().push(record);
}
pub fn add_transaction(&self, tx: Transaction) -> Txid {
let txid = tx.compute_txid();
self.transactions.write().insert(txid, tx);
txid
}
#[must_use]
pub fn height(&self) -> u32 {
self.chain_tip.load_full().map_or(0, |tip| tip.height)
}
#[must_use]
pub fn applied_height(&self) -> u32 {
self.applied_tip.load_full().map_or(0, |tip| tip.height)
}
#[must_use]
pub fn applied_hash(&self) -> Hash256 {
self.applied_tip
.load_full()
.map_or_else(Hash256::default, |tip| tip.hash)
}
#[must_use]
pub fn best_hash(&self) -> Hash256 {
self.chain_tip
.load_full()
.map_or_else(Hash256::default, |tip| tip.hash)
}
#[must_use]
pub fn chainwork_hex(&self) -> String {
let Some(tip) = self.chain_tip.load_full() else {
return "00".to_owned();
};
let bytes: [u8; 32] = tip.chainwork.to_be_bytes();
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use core::fmt::Write as _;
let _: fmt::Result = write!(&mut out, "{byte:02x}");
}
out
}
#[must_use]
pub fn block_hash_at_height(&self, height: u32) -> Option<Hash256> {
self.blocks
.read()
.iter()
.find(|record| record.height == height)
.map(|record| record.hash)
.or_else(|| {
self.chain_tip.load_full().and_then(|tip| {
if tip.height == height {
Some(tip.hash)
} else {
None
}
})
})
.or_else(|| {
if height == 0 {
let genesis_hash = bitcoin::blockdata::constants::genesis_block(
bitcoin_network(self.chain_network),
)
.block_hash();
Some(Hash256::from_le_bytes(genesis_hash.as_byte_array()))
} else {
None
}
})
}
#[must_use]
pub fn block_by_hash(&self, hash: Hash256) -> Option<BlockRecord> {
self.blocks
.read()
.iter()
.find(|record| record.hash == hash)
.cloned()
}
#[must_use]
pub fn block_by_height(&self, height: u32) -> Option<BlockRecord> {
self.blocks
.read()
.iter()
.find(|record| record.height == height)
.cloned()
}
#[must_use]
pub fn block_body_bytes(&self, record: &BlockRecord) -> Option<Vec<u8>> {
if !record.block_hex.is_empty() {
return Vec::<u8>::from_hex(&record.block_hex).ok();
}
self.block_body_source
.as_ref()?
.block_body(record.height, record.hash)
}
#[must_use]
pub fn block_body_hex(&self, record: &BlockRecord) -> Option<String> {
if !record.block_hex.is_empty() {
return Some(record.block_hex.clone());
}
Some(self.block_body_bytes(record)?.to_lower_hex_string())
}
#[must_use]
pub fn median_time_past_for_hash(&self, hash: bitcoin_rs_primitives::Hash256) -> Option<u32> {
let tree = self.block_tree.read();
let node_id = tree.lookup(hash)?;
tree.median_time_past_at(node_id, 11)
}
#[must_use]
pub fn height_for_hash(&self, hash: bitcoin_rs_primitives::Hash256) -> Option<u32> {
self.block_tree.read().height_of_hash(hash)
}
#[must_use]
pub fn chain_work_hex_for_hash(&self, hash: bitcoin_rs_primitives::Hash256) -> Option<String> {
let tree = self.block_tree.read();
let node = tree.node_by_hash(hash)?;
let bytes: [u8; 32] = node.chainwork.to_be_bytes();
Some(bytes.to_lower_hex_string())
}
#[must_use]
pub fn next_block_hash_for_height(
&self,
height: u32,
) -> Option<bitcoin_rs_primitives::Hash256> {
let tree = self.block_tree.read();
let tip = tree.tip()?;
let next_height = height.checked_add(1)?;
let node_id = tree.node_at_height_from(tip.tip_id, next_height)?;
let node = tree.node(node_id).ok()?;
Some(node.hash)
}
}
impl bitcoin_rs_index::BlockSource for Context {
fn block_at_height(&self, height: u32) -> Option<bitcoin::Block> {
let record = self
.blocks
.read()
.iter()
.find(|record| record.height == height)
.cloned()?;
let bytes = self.block_body_bytes(&record)?;
bitcoin::consensus::encode::deserialize::<bitcoin::Block>(&bytes).ok()
}
}
fn bitcoin_network(network: Network) -> bitcoin::Network {
match network {
Network::Mainnet => bitcoin::Network::Bitcoin,
Network::Testnet3 => bitcoin::Network::Testnet,
Network::Testnet4 => bitcoin::Network::Testnet4,
Network::Signet => bitcoin::Network::Signet,
Network::Regtest => bitcoin::Network::Regtest,
}
}
fn target_to_f64(target: bitcoin::pow::Target) -> f64 {
target
.to_be_bytes()
.iter()
.fold(0.0_f64, |acc, &byte| acc.mul_add(256.0, f64::from(byte)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::arc_with_non_send_sync)]
fn from_handles_shares_tip_handles_with_caller() {
use alloc::sync::Arc;
let chain_tip = Arc::new(ArcSwapOption::empty());
let applied_tip = Arc::new(ArcSwapOption::empty());
let utxo = Arc::new(bitcoin_rs_utxo::UtxoSet::new());
let coin_stats = Arc::new(bitcoin_rs_coinstats::CoinStatsListener::new(
bitcoin_rs_coinstats::CoinStats::default(),
));
let filter_index = noop_filter_index();
let block_tree = Arc::new(RwLock::new(bitcoin_rs_chain::BlockTree::new()));
let banned = Arc::new(RwLock::new(Vec::<bitcoin_rs_p2p::BannedSubnet>::new()));
let added_nodes = Arc::new(RwLock::new(Vec::new()));
let ctx = Context::from_handles(
Arc::clone(&chain_tip),
Arc::clone(&applied_tip),
Arc::new(RwLock::new(Mempool::new(MempoolLimits::default()))),
Arc::new(RwLock::new(Vec::new())),
Arc::new(RwLock::new(HashMap::new())),
Arc::clone(&utxo),
Arc::clone(&coin_stats),
Arc::clone(&filter_index),
Arc::new(RwLock::new(NetworkState::default())),
Arc::new(ArcSwap::from_pointee(CompactString::new("0"))),
Arc::new(RwLock::new(Vec::new())),
Arc::clone(&block_tree),
Network::Mainnet,
None,
None,
Arc::clone(&banned),
Arc::clone(&added_nodes),
None,
);
assert!(
Arc::ptr_eq(&ctx.chain_tip, &chain_tip),
"chain_tip must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.applied_tip, &applied_tip),
"applied_tip must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.utxo, &utxo),
"utxo must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.coin_stats, &coin_stats),
"coin_stats must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.filter_index, &filter_index),
"filter_index must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.block_tree, &block_tree),
"block_tree must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.banned, &banned),
"banned must be shared with caller"
);
assert!(
Arc::ptr_eq(&ctx.added_nodes, &added_nodes),
"added_nodes must be shared with caller"
);
}
#[test]
fn new_context_wires_utxo_commits_to_coin_stats() {
use bitcoin::{Amount, ScriptBuf};
use bitcoin_rs_primitives::{Hash256, OutPoint, TxOut};
use bitcoin_rs_utxo::{BlockChanges, UtxoAdd};
let ctx = Context::new();
let outpoint = OutPoint::new(Hash256::from_le_bytes(&[1_u8; 32]), 0);
let txout = TxOut {
value: Amount::from_sat(125_000),
script_pubkey: ScriptBuf::new(),
};
let mut changes = BlockChanges::default();
changes.add(UtxoAdd::new(outpoint, txout, true, 7));
ctx.utxo
.commit_block(&changes, &Hash256::default())
.unwrap_or_else(|err| panic!("commit_block failed: {err}"));
let snapshot = ctx.coin_stats.snapshot();
assert_eq!(snapshot.utxo_count, 1);
assert_eq!(snapshot.total_amount, 125_000);
}
#[test]
fn block_record_from_block_bytes_matches_from_block() {
let block = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest);
let block_bytes = serialize(&block);
let from_block = BlockRecord::from_block(0, &block);
let from_bytes = BlockRecord::from_block_bytes(0, &block, &block_bytes);
assert_eq!(from_bytes.hash, from_block.hash);
assert_eq!(from_bytes.height, from_block.height);
assert_eq!(from_bytes.block_hex, from_block.block_hex);
assert_eq!(from_bytes.body_size, from_block.body_size);
assert_eq!(from_bytes.header_hex, from_block.header_hex);
assert_eq!(from_bytes.tx_count, from_block.tx_count);
assert_eq!(from_bytes.time, from_block.time);
}
#[test]
fn context_reads_metadata_only_block_record_from_body_source() {
struct SingleBlockSource {
height: u32,
hash: Hash256,
body: Vec<u8>,
}
impl BlockBodySource for SingleBlockSource {
fn block_body(&self, height: u32, hash: Hash256) -> Option<Vec<u8>> {
(height == self.height && hash == self.hash).then(|| self.body.clone())
}
}
let block = bitcoin::blockdata::constants::genesis_block(bitcoin::Network::Regtest);
let body = serialize(&block);
let record = BlockRecord::from_block_metadata(0, &block);
let source = Arc::new(SingleBlockSource {
height: 0,
hash: record.hash,
body: body.clone(),
});
let ctx = Context::new().with_block_body_source(source);
ctx.add_block(record.clone());
assert!(record.block_hex.is_empty());
assert_eq!(record.body_size, body.len());
assert_eq!(
ctx.block_body_bytes(&record).as_deref(),
Some(body.as_slice())
);
let expected_hex = body.to_lower_hex_string();
assert_eq!(
ctx.block_body_hex(&record).as_deref(),
Some(expected_hex.as_str())
);
}
#[test]
fn block_by_height_returns_record_after_add_block() {
use bitcoin_rs_primitives::Hash256;
let ctx = Context::new();
let record = BlockRecord::synthetic(42, Hash256::default());
ctx.add_block(record);
let Some(found) = ctx.block_by_height(42) else {
panic!("expected record at height 42");
};
assert_eq!(found.height, 42);
}
#[test]
fn height_for_hash_returns_none_when_tree_empty() {
let ctx = Context::new();
let unknown = bitcoin_rs_primitives::Hash256::from_le_bytes(&[0xff_u8; 32]);
assert!(ctx.height_for_hash(unknown).is_none());
}
}