use crate::config::RequestTimeoutConfig;
use crate::node::block_processor::{prepare_block_validation_context, validate_block_with_context};
use crate::node::event_publisher::EventPublisher;
use crate::rpc::errors::{
BLOCK_HASH_PARAM_REQUIRED_MSG, BLOCK_NOT_FOUND_MSG, BlockNotFoundError,
HEIGHT_PARAM_REQUIRED_MSG, RpcError, STORAGE_NOT_AVAILABLE_MSG,
};
use crate::rpc::params::{param_str_required, param_u64, param_u64_default};
use crate::storage::Storage;
use crate::storage::assumeutxo::AssumeUtxoManager;
use crate::utils::{
CACHE_REFRESH_TIP, POLL_INTERVAL_WAIT_FOR_BLOCK, storage_timeout_from_config,
with_custom_timeout,
};
use anyhow::Result;
use serde_json::{Number, Value, json};
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, warn};
const ZERO_HASH_STR: &str = "0000000000000000000000000000000000000000000000000000000000000000";
fn non_coinbase_fee_sats(
block: &blvm_protocol::Block,
undo: &blvm_consensus::reorganization::BlockUndoLog,
) -> Vec<u64> {
let mut spent: std::collections::HashMap<blvm_protocol::OutPoint, u64> =
std::collections::HashMap::new();
for entry in &undo.entries {
if let Some(prev) = &entry.previous_utxo {
if let Ok(value) = u64::try_from(prev.value) {
spent.insert(entry.outpoint, value);
}
}
}
let mut fees = Vec::new();
for tx in block.transactions.iter().skip(1) {
let mut inputs = 0u64;
let mut known = true;
for input in &tx.inputs {
match spent.get(&input.prevout) {
Some(value) => inputs = inputs.saturating_add(*value),
None => {
known = false;
break;
}
}
}
if !known {
continue;
}
let outputs: u64 = tx
.outputs
.iter()
.filter_map(|output| u64::try_from(output.value).ok())
.sum();
fees.push(inputs.saturating_sub(outputs));
}
fees
}
thread_local! {
static CACHED_TIP_HEIGHT: crate::rpc::cache::ThreadLocalTimedCache<u64> =
crate::rpc::cache::ThreadLocalTimedCache::new();
static CACHED_TIP_HASH_HEX: crate::rpc::cache::ThreadLocalKeyedCache<String, ([u8; 32], u64)> =
crate::rpc::cache::ThreadLocalKeyedCache::new();
static CACHED_DIFFICULTY: crate::rpc::cache::ThreadLocalKeyedCache<f64, u64> =
crate::rpc::cache::ThreadLocalKeyedCache::new();
}
fn decode_hash32(hex: &str) -> Result<[u8; 32], RpcError> {
crate::storage::hashing::hash_from_rpc_hex(hex)
.map_err(|e| RpcError::invalid_hash_format(hex, Some(32), Some(&e)))
}
fn encode_hash32_rpc(hash: &[u8; 32]) -> String {
crate::storage::hashing::hash_to_rpc_hex(hash)
}
fn spent_scripts_from_undo(storage: &Storage, hash: &blvm_protocol::Hash) -> Vec<Vec<u8>> {
let mut scripts = Vec::new();
#[cfg(feature = "production")]
if let Ok(Some(undo)) = storage.blocks().get_undo_log(hash) {
for entry in &undo.entries {
if let Some(prev) = &entry.previous_utxo {
scripts.push(prev.script_pubkey.as_ref().to_vec());
}
}
}
#[cfg(not(feature = "production"))]
let _ = (storage, hash);
scripts
}
fn previous_filter_header(storage: &Storage, header: &blvm_protocol::BlockHeader) -> [u8; 32] {
storage
.blocks()
.get_filter_header(&header.prev_block_hash)
.ok()
.flatten()
.unwrap_or([0u8; 32])
}
fn chain_filter_header(filter: &[u8], prev: &[u8; 32]) -> [u8; 32] {
let mut buf = Vec::with_capacity(filter.len() + 32);
buf.extend_from_slice(filter);
buf.extend_from_slice(prev);
crate::storage::hashing::double_sha256(&buf)
}
fn filter_index_synced(storage: &Storage, tip_height: u64) -> bool {
let Ok(Some(tip_hash)) = storage.chain().get_tip_hash() else {
return false;
};
if tip_hash == blvm_protocol::Hash::default() {
return false;
}
for height in (0..=tip_height).rev() {
let Ok(Some(hash)) = storage.blocks().get_hash_by_height(height) else {
return false;
};
if storage
.blocks()
.get_filter_header(&hash)
.ok()
.flatten()
.is_none()
{
return false;
}
}
true
}
fn in_money_range(value: i64) -> bool {
const MAX_MONEY: i64 = 21_000_000 * 100_000_000;
(0..=MAX_MONEY).contains(&value)
}
#[derive(Clone)]
pub struct BlockchainRpc {
storage: Option<Arc<Storage>>,
protocol: Option<Arc<blvm_protocol::BitcoinProtocolEngine>>,
event_publisher: Option<Arc<EventPublisher>>,
request_timeouts: Option<RequestTimeoutConfig>,
}
impl Default for BlockchainRpc {
fn default() -> Self {
Self::new()
}
}
impl BlockchainRpc {
pub fn new() -> Self {
Self {
storage: None,
protocol: None,
event_publisher: None,
request_timeouts: None,
}
}
pub fn with_dependencies(storage: Arc<Storage>) -> Self {
Self {
storage: Some(storage),
protocol: None,
event_publisher: None,
request_timeouts: None,
}
}
pub fn require_storage(&self) -> Result<&Arc<Storage>, RpcError> {
self.storage
.as_ref()
.ok_or_else(RpcError::storage_not_available)
}
pub fn with_event_publisher(mut self, event_publisher: Option<Arc<EventPublisher>>) -> Self {
self.event_publisher = event_publisher;
self
}
pub fn with_request_timeouts(mut self, config: Option<RequestTimeoutConfig>) -> Self {
self.request_timeouts = config;
self
}
pub fn with_dependencies_and_protocol(
storage: Arc<Storage>,
protocol: Arc<blvm_protocol::BitcoinProtocolEngine>,
) -> Self {
Self {
storage: Some(storage),
protocol: Some(protocol),
event_publisher: None,
request_timeouts: None,
}
}
fn storage_timeout(&self) -> std::time::Duration {
storage_timeout_from_config(self.request_timeouts.as_ref())
}
fn get_chain_name(&self) -> &str {
self.protocol
.as_ref()
.map(|p| match p.get_protocol_version() {
blvm_protocol::ProtocolVersion::BitcoinV1 => "mainnet",
blvm_protocol::ProtocolVersion::Testnet3 => "testnet",
blvm_protocol::ProtocolVersion::Regtest => "regtest",
blvm_protocol::ProtocolVersion::Signet => "signet",
blvm_protocol::ProtocolVersion::Testnet4 => "testnet4",
})
.unwrap_or("regtest") }
fn core_chain_field(&self) -> &str {
if let Some(ref storage) = self.storage {
if let Ok(Some(info)) = storage.chain().load_chain_info() {
return match info.chain_params.network.as_str() {
"mainnet" => "main",
"testnet" => "test",
"testnet4" => "testnet4",
"regtest" => "regtest",
"signet" => "signet",
_ => "main",
};
}
}
match self.get_chain_name() {
"mainnet" => "main",
"testnet" => "test",
"testnet4" => "testnet4",
"regtest" => "regtest",
"signet" => "signet",
_ => "main",
}
}
fn calculate_difficulty(bits: u64) -> f64 {
blvm_protocol::pow::difficulty_from_bits(bits).unwrap_or(1.0)
}
fn median_time_through_height(&self, storage: &Storage, height: u64, fallback: u64) -> u64 {
match storage.blocks().headers_back_from(height, 11) {
Ok(headers) if !headers.is_empty() => {
blvm_protocol::bip113::get_median_time_past(&headers)
}
_ => fallback,
}
}
fn block_subsidy(&self, height: u64) -> u64 {
let network = crate::storage::resolve_consensus_network(
self.protocol.as_deref(),
self.storage.as_deref(),
);
blvm_protocol::ConsensusProof::new().get_block_subsidy_for_network(height, network) as u64
}
fn compute_block_wire_sizes(
block: &blvm_protocol::Block,
witnesses: Option<&[Vec<blvm_protocol::segwit::Witness>]>,
) -> (usize, usize, u64) {
use blvm_protocol::serialization::serialize_block_with_witnesses;
use blvm_protocol::serialization::transaction::serialize_transaction;
let stripped_size: usize = block
.transactions
.iter()
.map(|tx| serialize_transaction(tx).len())
.sum::<usize>()
+ 80;
let block_size = if let Some(ws) = witnesses {
let wire = serialize_block_with_witnesses(block, ws, true);
wire.len().max(stripped_size)
} else {
stripped_size
};
let block_weight = (3 * stripped_size + block_size) as u64;
(stripped_size, block_size, block_weight)
}
fn resolve_script_pubkey_param(address: &str) -> Result<Vec<u8>, RpcError> {
if let Ok(script) = hex::decode(address) {
if !script.is_empty() {
return Ok(script);
}
}
crate::rpc::rawtx::address_string_to_script_pubkey(address)
}
fn calculate_utxo_muhash(utxo_set: &blvm_protocol::UtxoSet) -> [u8; 32] {
crate::storage::assumeutxo::AssumeUtxoManager::calculate_utxo_hash(utxo_set)
.unwrap_or([0u8; 32])
}
fn calculate_confirmations(block_height: u64, tip_height: u64) -> i64 {
if block_height > tip_height {
return 0;
}
(tip_height - block_height + 1) as i64
}
fn format_chainwork(work: blvm_consensus::pow::U256) -> String {
work.gbt_target_hex()
}
fn softforks_json(&self, height: u64) -> Value {
use blvm_consensus::activation::ForkActivationTable;
use blvm_consensus::types::Network;
let network = match self.core_chain_field() {
"test" => Network::Testnet,
"testnet4" => Network::Testnet4,
"regtest" => Network::Regtest,
"signet" => Network::Signet,
_ => Network::Mainnet,
};
let table = ForkActivationTable::from_network(network);
json!({
"segwit": {
"type": "buried",
"active": height >= table.segwit,
"height": table.segwit
},
"taproot": {
"type": "buried",
"active": height >= table.taproot,
"height": table.taproot
}
})
}
pub async fn get_blockchain_info(&self) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getblockchaininfo");
let softforks = self.softforks_json(0);
if let Some(ref storage) = self.storage {
let (best_hash, height) = storage.chain().get_tip_hash_and_height()?;
let header_height = storage
.blocks()
.highest_stored_height()
.ok()
.flatten()
.unwrap_or(height);
let softforks = self.softforks_json(height);
let best_hash_hex = CACHED_TIP_HASH_HEX.with(|c| {
c.get_or_refresh(CACHE_REFRESH_TIP, &(best_hash, height), || {
encode_hash32_rpc(&best_hash)
})
});
let difficulty = if let Ok(Some(tip_header)) = storage.chain().get_tip_header() {
Self::calculate_difficulty(tip_header.bits)
} else {
1.0
};
let mediantime = self.median_time_through_height(storage, height, 0);
let chainwork = storage
.chain()
.get_chainwork(&best_hash)?
.unwrap_or_else(blvm_consensus::pow::U256::zero);
let chainwork_hex = Self::format_chainwork(chainwork);
Ok(json!({
"chain": self.core_chain_field(),
"blocks": height,
"headers": header_height,
"bestblockhash": best_hash_hex,
"difficulty": difficulty,
"mediantime": mediantime,
"verificationprogress": 0.0,
"initialblockdownload": true,
"chainwork": chainwork_hex,
"size_on_disk": if let Some(ref storage) = self.storage {
storage.disk_size().unwrap_or(0)
} else {
0
},
"pruned": if let Some(ref storage) = self.storage {
storage.is_pruning_enabled()
} else {
false
},
"pruneheight": if let Some(ref storage) = self.storage {
if let Some(pruning_manager) = storage.pruning() {
pruning_manager.get_stats().last_prune_height.unwrap_or(0)
} else {
0
}
} else {
0
},
"automatic_pruning": if let Some(ref storage) = self.storage {
if let Some(pruning_manager) = storage.pruning() {
pruning_manager.config.auto_prune
} else {
false
}
} else {
false
},
"softforks": softforks,
"warnings": ""
}))
} else {
tracing::debug!(
"getblockchaininfo called but storage not available, returning default values"
);
Ok(json!({
"chain": self.core_chain_field(),
"blocks": 0,
"headers": 0,
"bestblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
"difficulty": 1.0,
"mediantime": 1231006505,
"verificationprogress": 0.0,
"initialblockdownload": true,
"chainwork": "0000000000000000000000000000000000000000000000000000000000000000",
"size_on_disk": 0,
"pruned": false,
"pruneheight": 0,
"automatic_pruning": false,
"softforks": softforks,
"warnings": format!("{} - returning default values", STORAGE_NOT_AVAILABLE_MSG.split('.').next().unwrap_or(STORAGE_NOT_AVAILABLE_MSG))
}))
}
}
pub async fn get_block(&self, hash: &str) -> Result<Value> {
debug!("RPC: getblock {}", hash);
let hash_array = decode_hash32(hash).map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(ref storage) = self.storage {
let timeout_dur = self.storage_timeout();
match with_custom_timeout(
async {
tokio::task::spawn_blocking({
let storage = storage.clone();
move || storage.blocks().get_block(&hash_array)
})
.await
},
timeout_dur,
)
.await
{
Ok(Ok(Ok(Some(block)))) => {
let block_height = storage.blocks().get_height_by_hash(&hash_array)?;
let tip_height = CACHED_TIP_HEIGHT.with(|c| {
c.get_or_refresh(CACHE_REFRESH_TIP, || {
storage
.chain()
.get_height()
.map(|h| h.unwrap_or(0))
.unwrap_or(0)
})
});
let confirmations = block_height
.map(|h| Self::calculate_confirmations(h, tip_height))
.unwrap_or(0);
let witnesses = storage.blocks().get_witness(&hash_array).ok().flatten();
let (stripped_size, block_size, block_weight) =
Self::compute_block_wire_sizes(&block, witnesses.as_deref());
let mediantime = match block_height {
Some(height) => {
self.median_time_through_height(storage, height, block.header.timestamp)
}
None => block.header.timestamp,
};
let difficulty = Self::calculate_difficulty(block.header.bits);
let tx_ids: Vec<String> = block
.transactions
.iter()
.map(|tx| {
let tx_hash = blvm_protocol::block::calculate_tx_id(tx);
encode_hash32_rpc(&tx_hash)
})
.collect();
let next_blockhash = block_height.and_then(|h| {
storage
.blocks()
.get_hash_by_height(h + 1)
.ok()
.flatten()
.map(|h| encode_hash32_rpc(&h))
});
let chainwork = if let Some(_height) = block_height {
storage
.chain()
.get_chainwork(&hash_array)?
.map(Self::format_chainwork)
.unwrap_or_else(|| ZERO_HASH_STR.to_string())
} else {
ZERO_HASH_STR.to_string()
};
return Ok(json!({
"hash": hash,
"confirmations": confirmations,
"size": block_size,
"strippedsize": stripped_size,
"weight": block_weight,
"height": block_height.unwrap_or(0),
"version": block.header.version,
"versionHex": format!("{:08x}", block.header.version),
"merkleroot": encode_hash32_rpc(&block.header.merkle_root),
"tx": tx_ids,
"time": block.header.timestamp,
"mediantime": mediantime,
"nonce": block.header.nonce as u32,
"bits": format!("{:08x}", block.header.bits),
"difficulty": difficulty,
"chainwork": chainwork,
"nTx": block.transactions.len(),
"previousblockhash": encode_hash32_rpc(&block.header.prev_block_hash),
"nextblockhash": next_blockhash.unwrap_or_else(|| ZERO_HASH_STR.to_string()),
}));
}
Ok(Ok(Ok(None))) => {
return Err(BlockNotFoundError::new(hash).into());
}
Ok(Ok(Err(e))) => {
warn!("Storage error getting block {}: {}", hash, e);
}
Ok(Err(_)) => {
warn!("Task error getting block {}", hash);
}
Err(_) => {
warn!("Timeout getting block {} from storage", hash);
}
}
}
Err(anyhow::anyhow!(
"{} or storage unavailable",
BLOCK_NOT_FOUND_MSG
))
}
pub async fn get_block_hash(&self, height: u64) -> Result<Value> {
debug!("RPC: getblockhash {}", height);
if let Some(ref storage) = self.storage {
if let Ok(Some(hash)) = storage.blocks().get_hash_by_height(height) {
return Ok(Value::String(encode_hash32_rpc(&hash)));
}
return Err(BlockNotFoundError::new(format!("at height {height}")).into());
}
Err(anyhow::anyhow!("Storage not available"))
}
pub async fn get_block_header(&self, hash: &str, verbose: bool) -> Result<Value> {
debug!("RPC: getblockheader {} verbose={}", hash, verbose);
if let Some(ref storage) = self.storage {
let hash_array = decode_hash32(hash)?;
if let Ok(Some(header)) = storage.blocks().get_header(&hash_array) {
if verbose {
let block_height = storage.blocks().get_height_by_hash(&hash_array)?;
let tip_height = CACHED_TIP_HEIGHT.with(|c| {
c.get_or_refresh(CACHE_REFRESH_TIP, || {
storage
.chain()
.get_height()
.map(|h| h.unwrap_or(0))
.unwrap_or(0)
})
});
let confirmations = block_height
.map(|h| Self::calculate_confirmations(h, tip_height))
.unwrap_or(0);
let mediantime = match block_height {
Some(height) => {
self.median_time_through_height(storage, height, header.timestamp)
}
None => header.timestamp,
};
let difficulty = Self::calculate_difficulty(header.bits);
let n_tx = storage
.blocks()
.get_block_metadata(&hash_array)?
.map(|m| m.n_tx as usize)
.unwrap_or(0);
let next_blockhash = block_height.and_then(|h| {
storage
.blocks()
.get_hash_by_height(h + 1)
.ok()
.flatten()
.map(|h| encode_hash32_rpc(&h))
});
let chainwork = if let Some(_height) = block_height {
storage
.chain()
.get_chainwork(&hash_array)?
.map(Self::format_chainwork)
.unwrap_or_else(|| ZERO_HASH_STR.to_string())
} else {
ZERO_HASH_STR.to_string()
};
Ok(json!({
"hash": hash,
"confirmations": confirmations,
"height": block_height.unwrap_or(0),
"version": header.version,
"versionHex": format!("{:08x}", header.version),
"merkleroot": encode_hash32_rpc(&header.merkle_root),
"time": header.timestamp,
"mediantime": mediantime,
"nonce": header.nonce as u32,
"bits": hex::encode(header.bits.to_le_bytes()),
"difficulty": difficulty,
"chainwork": chainwork,
"nTx": n_tx,
"previousblockhash": encode_hash32_rpc(&header.prev_block_hash),
"nextblockhash": next_blockhash
}))
} else {
use blvm_protocol::serialization::serialize_block_header;
let header_bytes = serialize_block_header(&header);
Ok(Value::String(hex::encode(header_bytes)))
}
} else {
Err(BlockNotFoundError::new("").into())
}
} else if verbose {
Ok(json!({
"hash": hash,
"confirmations": 0,
"height": 0,
"version": 1,
"versionHex": "00000001",
"merkleroot": "0000000000000000000000000000000000000000000000000000000000000000",
"time": 1231006505,
"mediantime": 1231006505,
"nonce": 0,
"bits": "1d00ffff",
"difficulty": 1.0,
"chainwork": "0000000000000000000000000000000000000000000000000000000000000000",
"nTx": 0,
"previousblockhash": null,
"nextblockhash": null
}))
} else {
Ok(json!(
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
))
}
}
pub async fn get_best_block_hash(&self) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getbestblockhash");
if let Some(ref storage) = self.storage {
let current_height = storage.chain().get_height()?.unwrap_or(0);
if let Ok(Some(hash)) = storage.chain().get_tip_hash() {
let hex_str = CACHED_TIP_HASH_HEX.with(|c| {
c.get_or_refresh(CACHE_REFRESH_TIP, &(hash, current_height), || {
encode_hash32_rpc(&hash)
})
});
Ok(Value::String(hex_str))
} else {
Ok(Value::String(ZERO_HASH_STR.to_string()))
}
} else {
Ok(Value::String(ZERO_HASH_STR.to_string()))
}
}
pub async fn get_block_count(&self) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getblockcount");
if let Some(ref storage) = self.storage {
let height = storage.chain().get_height()?.unwrap_or(0);
Ok(Value::Number(Number::from(height)))
} else {
Ok(Value::Number(Number::from(0)))
}
}
pub async fn get_difficulty(&self) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getdifficulty");
if let Some(ref storage) = self.storage {
let current_height = storage.chain().get_height()?.unwrap_or(0);
let difficulty = CACHED_DIFFICULTY.with(|c| {
c.get_or_refresh(CACHE_REFRESH_TIP, ¤t_height, || {
storage
.chain()
.get_tip_header()
.ok()
.flatten()
.map(|h| Self::calculate_difficulty(h.bits))
.unwrap_or(1.0)
})
});
Ok(Value::Number(
Number::from_f64(difficulty).unwrap_or_else(|| Number::from(1)),
))
} else {
Ok(Value::Number(Number::from_f64(1.0).unwrap()))
}
}
pub async fn get_txoutset_info(&self) -> Result<Value> {
debug!("RPC: gettxoutsetinfo");
if let Some(ref storage) = self.storage {
let (height, best_hash) = {
let (hash, h) = storage.chain().get_tip_hash_and_height()?;
(h, hash)
};
if let Ok(Some(stats)) = storage.chain().get_latest_utxo_stats() {
Ok(json!({
"height": stats.height,
"bestblock": encode_hash32_rpc(&best_hash),
"transactions": stats.transactions,
"txouts": stats.txouts,
"bogosize": stats.txouts * 180,
"muhash": hex::encode(stats.muhash),
"disk_size": storage.disk_size().unwrap_or(0),
"total_amount": stats.total_amount as f64 / 100_000_000.0
}))
} else {
let utxos = storage.utxos().get_all_utxos()?;
let txouts = utxos.len();
let total_amount = crate::storage::chainstate::utxo_money_total(
utxos.values().map(|utxo| utxo.value),
);
let muhash = Self::calculate_utxo_muhash(&utxos);
Ok(json!({
"height": height,
"bestblock": encode_hash32_rpc(&best_hash),
"transactions": storage.transaction_count().unwrap_or(0),
"txouts": txouts,
"bogosize": txouts * 180,
"muhash": hex::encode(muhash),
"disk_size": storage.disk_size().unwrap_or(0),
"total_amount": total_amount as f64 / 100_000_000.0
}))
}
} else {
Ok(json!({
"height": 0,
"bestblock": ZERO_HASH_STR,
"transactions": 0,
"txouts": 0,
"bogosize": 0,
"muhash": ZERO_HASH_STR,
"disk_size": 0,
"total_amount": 0.0
}))
}
}
pub async fn load_txout_set(&self, params: &Value) -> Result<Value> {
debug!("RPC: loadtxoutset");
let path_str = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| RpcError::invalid_params("loadtxoutset requires path (string)"))?;
let path = Path::new(path_str);
let manager = AssumeUtxoManager::new(".");
let (_utxo_set, metadata) = manager
.load_snapshot_from_path(path)
.map_err(|e| RpcError::internal_error(e.to_string()))?;
Ok(json!({
"base_blockhash": encode_hash32_rpc(&metadata.block_hash),
"height": metadata.block_height,
"txout_count": metadata.utxo_count,
"utxo_hash": hex::encode(metadata.utxo_hash),
}))
}
pub async fn verify_chain(
&self,
checklevel: Option<u64>,
numblocks: Option<u64>,
) -> Result<Value> {
debug!(
"RPC: verifychain checklevel={:?} numblocks={:?}",
checklevel, numblocks
);
if let Some(ref storage) = self.storage {
let check_level = checklevel.unwrap_or(3);
let num_blocks = numblocks.unwrap_or(288);
let tip_height = storage.chain().get_height()?.unwrap_or(0);
if tip_height == 0 {
return Ok(json!(true)); }
let start_height = tip_height.saturating_sub(num_blocks);
let mut errors = Vec::new();
let blockstore = storage.blocks();
#[cfg(feature = "production")]
let mut utxo_set_for_level4: Option<blvm_protocol::UtxoSet> = None;
#[cfg(feature = "production")]
let engine_arc_opt = self.protocol.as_ref();
#[cfg(feature = "production")]
if check_level >= 4 {
if let Some(_engine_arc) = engine_arc_opt {
let mut utxo_set = storage
.utxos()
.get_all_utxos()
.map_err(|e| anyhow::anyhow!("Failed to get UTXO set: {}", e))?;
let mut rewind_failed = false;
for height in (start_height..=tip_height).rev() {
if let Ok(Some(block_hash)) = storage.blocks().get_hash_by_height(height) {
match blockstore.get_undo_log(&block_hash) {
Ok(Some(undo_log)) => {
for entry in undo_log.entries.iter() {
if entry.new_utxo.is_some() {
utxo_set.remove(&entry.outpoint);
}
if let Some(ref prev_utxo) = entry.previous_utxo {
utxo_set.insert(entry.outpoint, prev_utxo.clone());
}
}
}
Ok(None) => {
errors.push(format!(
"Level 4 validation requires undo logs; missing for height {height}"
));
rewind_failed = true;
break;
}
Err(e) => {
errors.push(format!(
"Failed to load undo log for height {height}: {e}"
));
rewind_failed = true;
break;
}
}
}
}
if !rewind_failed {
utxo_set_for_level4 = Some(utxo_set);
}
} else {
errors.push("Level 4 validation requires protocol engine".to_string());
}
}
for height in start_height..=tip_height {
if let Ok(Some(block_hash)) = storage.blocks().get_hash_by_height(height) {
if let Ok(Some(block)) = storage.blocks().get_block(&block_hash) {
if check_level >= 3 && height > 0 {
if let Ok(Some(prev_hash)) =
storage.blocks().get_hash_by_height(height - 1)
{
if block.header.prev_block_hash != prev_hash {
errors.push(format!(
"Block at height {} has incorrect prev_block_hash: expected {}, got {}",
height,
hex::encode(prev_hash),
hex::encode(block.header.prev_block_hash)
));
if check_level >= 4 {
break;
}
}
}
}
if check_level >= 2 {
use blvm_protocol::mining::{
calculate_merkle_root, calculate_merkle_root_from_tx_ids,
};
let calculated_root = crate::module::pipeline::try_get_canonical_txids(
height, block_hash,
)
.and_then(|txids| calculate_merkle_root_from_tx_ids(&txids).ok())
.or_else(|| calculate_merkle_root(&block.transactions).ok());
if let Some(calculated_root) = calculated_root {
if calculated_root != block.header.merkle_root {
errors.push(format!(
"Block at height {height} has incorrect merkle root"
));
if check_level >= 4 {
break;
}
}
}
}
#[cfg(feature = "production")]
if check_level >= 4 {
if let Some(ref mut utxo_set) = utxo_set_for_level4 {
if let Some(engine_arc) = engine_arc_opt {
let protocol_version = engine_arc.get_protocol_version();
let witnesses =
match crate::node::block_processor::load_witnesses_for_block(
blockstore.as_ref(),
&block,
height,
protocol_version,
) {
Ok(w) => w,
Err(e) => {
errors.push(format!(
"Block at height {height} witness load error: {e}"
));
break;
}
};
let mtp_headers = blockstore
.headers_before_height_for_mtp(height)
.ok()
.filter(|h| !h.is_empty());
let median_time_past = mtp_headers
.as_ref()
.map(|h| blvm_protocol::bip113::get_median_time_past(h))
.unwrap_or(0);
let network_time = crate::utils::current_timestamp();
let mut context = match blvm_protocol::validation::ProtocolValidationContext::new(
protocol_version,
height,
) {
Ok(c) => c,
Err(e) => {
errors.push(format!(
"Block at height {height} context error: {e}"
));
break;
}
};
context.context_data.insert(
"median_time_past".to_string(),
median_time_past.to_string(),
);
context.context_data.insert(
"network_time".to_string(),
network_time.to_string(),
);
let owned_utxo = std::mem::take(utxo_set);
match engine_arc.validate_and_connect_block_with_difficulty(
&block,
&witnesses,
&owned_utxo,
height,
mtp_headers.as_deref(),
&context,
Some(blockstore.difficulty_ancestor_lookup()),
) {
Ok((blvm_protocol::ValidationResult::Valid, new_utxo)) => {
*utxo_set = new_utxo;
}
Ok((
blvm_protocol::ValidationResult::Invalid(reason),
_,
)) => {
errors.push(format!(
"Block at height {height} invalid: {reason}"
));
break;
}
Err(e) => {
errors.push(format!(
"Block at height {height} validation error: {e}"
));
break;
}
}
}
}
}
} else {
errors.push(format!("Block at height {height} not found in storage"));
if check_level >= 4 {
break;
}
}
} else {
errors.push(format!("Block hash at height {height} not found"));
if check_level >= 4 {
break;
}
}
}
if errors.is_empty() {
Ok(Value::Bool(true))
} else {
Ok(json!({
"valid": false,
"errors": errors,
"checked_blocks": (tip_height - start_height + 1)
}))
}
} else {
Ok(json!(true))
}
}
pub async fn get_chain_tips(&self) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getchaintips");
if let Some(ref storage) = self.storage {
let (tip_hash, _tip_height) = storage.chain().get_tip_hash_and_height()?;
let index = storage.chain().block_index();
let indexed_tips = index.chain_tips()?;
let mut tips = Vec::with_capacity(indexed_tips.len().max(1));
if indexed_tips.is_empty() {
if tip_hash != blvm_protocol::Hash::default() {
let height = storage.chain().get_height()?.unwrap_or(0);
tips.push(json!({
"height": height,
"hash": encode_hash32_rpc(&tip_hash),
"branchlen": 0,
"status": "active"
}));
}
} else {
for (hash, entry) in indexed_tips {
let branchlen = index.branch_length(&hash, &tip_hash)?;
let status = if storage.chain().is_invalid(&hash).unwrap_or(false)
|| entry.status == crate::storage::block_index::BlockIndexStatus::Invalid
{
"invalid"
} else if hash == tip_hash {
"active"
} else {
"valid"
};
tips.push(json!({
"height": entry.height,
"hash": encode_hash32_rpc(&hash),
"branchlen": branchlen,
"status": status
}));
}
tips.sort_by(|a, b| {
let ha = a.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
let hb = b.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
hb.cmp(&ha)
});
}
Ok(json!(tips))
} else {
Ok(json!([]))
}
}
pub async fn get_chain_tx_stats(&self, params: &Value) -> Result<Value> {
#[cfg(debug_assertions)]
debug!("RPC: getchaintxstats");
let nblocks = param_u64_default(params, 0, 144);
if let Some(ref storage) = self.storage {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
if tip_height == 0 {
return Ok(json!({
"time": 0,
"txcount": 0,
"window_final_block_height": 0,
"window_block_count": 0,
"window_tx_count": 0,
"window_interval": 0,
"txrate": 0.0
}));
}
let start_height = if tip_height >= nblocks {
tip_height - nblocks + 1
} else {
0
};
let range_size = (tip_height - start_height + 1) as usize;
let mut timestamps = Vec::with_capacity(range_size);
let mut tx_counts = Vec::with_capacity(range_size);
for height in start_height..=tip_height {
if let Ok(Some(hash)) = storage.blocks().get_hash_by_height(height) {
if let Ok(Some(header)) = storage.blocks().get_header(&hash) {
timestamps.push(header.timestamp);
let n_tx = storage
.blocks()
.get_block_metadata(&hash)
.ok()
.flatten()
.map(|m| m.n_tx as u64)
.unwrap_or(0);
tx_counts.push(n_tx);
}
}
}
if timestamps.is_empty() {
return Ok(json!({
"time": 0,
"txcount": 0,
"window_final_block_height": tip_height,
"window_block_count": 0,
"window_tx_count": 0,
"window_interval": 0,
"txrate": 0.0
}));
}
let first_timestamp = timestamps[0];
let last_timestamp = timestamps[timestamps.len() - 1];
let window_interval = last_timestamp.saturating_sub(first_timestamp);
let window_tx_count: u64 = tx_counts.iter().sum();
let window_block_count = timestamps.len() as u64;
let txrate = if window_interval > 0 {
window_tx_count as f64 / window_interval as f64
} else {
0.0
};
let total_tx_count: u64 = (0..=tip_height)
.filter_map(|height| {
storage
.blocks()
.get_hash_by_height(height)
.ok()
.flatten()
.and_then(|hash| {
storage
.blocks()
.get_block_metadata(&hash)
.ok()
.flatten()
.map(|m| m.n_tx as u64)
})
})
.sum();
Ok(json!({
"time": last_timestamp,
"txcount": total_tx_count,
"window_final_block_height": tip_height,
"window_block_count": window_block_count,
"window_tx_count": window_tx_count,
"window_interval": window_interval,
"txrate": txrate
}))
} else {
Ok(json!({
"time": 0,
"txcount": 0,
"window_final_block_height": 0,
"window_block_count": 0,
"window_tx_count": 0,
"window_interval": 0,
"txrate": 0.0
}))
}
}
pub async fn get_block_stats(&self, params: &Value) -> Result<Value> {
debug!("RPC: getblockstats");
let hash_or_height: Option<String> = params
.get(0)
.and_then(|p| p.as_u64().map(|h| h.to_string()))
.or_else(|| {
params
.get(0)
.and_then(|p| p.as_str())
.map(|s| s.to_string())
});
let hash_or_height = hash_or_height.as_deref();
if let Some(ref storage) = self.storage {
let block_hash = if let Some(hoh) = hash_or_height {
if let Ok(height) = hoh.parse::<u64>() {
storage
.blocks()
.get_hash_by_height(height)?
.ok_or_else(|| {
anyhow::Error::from(BlockNotFoundError::new(format!(
"at height {height}"
)))
})?
} else {
decode_hash32(hoh).map_err(|e| anyhow::anyhow!("Invalid block hash: {e}"))?
}
} else {
storage
.chain()
.get_tip_hash()?
.ok_or_else(|| anyhow::anyhow!("Chain not initialized"))?
};
if let Ok(Some(block)) = storage.blocks().get_block(&block_hash) {
let tx_count = block.transactions.len();
let witnesses = storage.blocks().get_witness(&block_hash).ok().flatten();
let (stripped_size, block_size, block_weight) =
Self::compute_block_wire_sizes(&block, witnesses.as_deref());
let height = storage
.blocks()
.get_height_by_hash(&block_hash)?
.unwrap_or(0);
let input_count: usize = block.transactions.iter().map(|tx| tx.inputs.len()).sum();
let output_count: usize =
block.transactions.iter().map(|tx| tx.outputs.len()).sum();
let total_out = u64::try_from(crate::storage::chainstate::utxo_money_total(
block
.transactions
.iter()
.flat_map(|tx| tx.outputs.iter().map(|out| out.value)),
))
.unwrap_or(u64::MAX);
let subsidy = self.block_subsidy(height);
let total_fees = if !block.transactions.is_empty() {
let coinbase_outputs =
u64::try_from(crate::storage::chainstate::utxo_money_total(
block.transactions[0].outputs.iter().map(|out| out.value),
))
.unwrap_or(u64::MAX);
coinbase_outputs.saturating_sub(subsidy)
} else {
0
};
let tx_vsize = |index: usize, tx: &blvm_protocol::Transaction| -> u64 {
let witness = witnesses
.as_ref()
.and_then(|all| all.get(index))
.map(|stack| stack.as_slice());
let (_, _, _, vsize) =
crate::rpc::rawtx::RawTxRpc::calculate_segwit_sizes(tx, witness);
(vsize as u64).max(1)
};
let non_coinbase_vsize: u64 = block
.transactions
.iter()
.enumerate()
.skip(1)
.map(|(index, tx)| tx_vsize(index, tx))
.sum();
let avgfeerate = if non_coinbase_vsize > 0 {
total_fees as f64 / non_coinbase_vsize as f64 * 1000.0 / 100_000_000.0
} else {
0.0
};
#[cfg(feature = "production")]
let tx_fees = storage
.blocks()
.get_undo_log(&block_hash)
.ok()
.flatten()
.map(|undo| non_coinbase_fee_sats(&block, &undo))
.unwrap_or_default();
#[cfg(not(feature = "production"))]
let tx_fees: Vec<u64> = Vec::new();
let mut sorted_fees = tx_fees.clone();
sorted_fees.sort_unstable();
let min_fee_sats = sorted_fees.first().copied().unwrap_or(0);
let max_fee_sats = sorted_fees.last().copied().unwrap_or(0);
let median_fee_sats = if sorted_fees.is_empty() {
0
} else {
sorted_fees[sorted_fees.len() / 2]
};
let fee_btc = |sats: u64| sats as f64 / 100_000_000.0;
let mut rates: Vec<f64> = block
.transactions
.iter()
.enumerate()
.skip(1)
.zip(tx_fees.iter())
.map(|((index, tx), fee)| {
*fee as f64 / tx_vsize(index, tx) as f64 * 1000.0 / 100_000_000.0
})
.collect();
rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let rate_at = |pct: usize| -> f64 {
if rates.is_empty() {
0.0
} else {
rates[pct * (rates.len() - 1) / 100]
}
};
let min_feerate = rates.first().copied().unwrap_or(0.0);
let max_feerate = rates.last().copied().unwrap_or(0.0);
Ok(json!({
"avgfee": if tx_count > 1 { total_fees as f64 / (tx_count - 1) as f64 / 100_000_000.0 } else { 0.0 },
"avgfeerate": avgfeerate,
"avgtxsize": if tx_count > 0 { block_size / tx_count } else { 0 },
"blockhash": encode_hash32_rpc(&block_hash),
"feerate_percentiles": [rate_at(10), rate_at(25), rate_at(50), rate_at(75), rate_at(90)],
"height": height,
"ins": input_count,
"maxfee": fee_btc(max_fee_sats),
"maxfeerate": max_feerate,
"maxtxsize": 0,
"medianfee": fee_btc(median_fee_sats),
"mediantime": self.median_time_through_height(storage, height, block.header.timestamp),
"mediantxsize": 0,
"minfee": fee_btc(min_fee_sats),
"minfeerate": min_feerate,
"mintxsize": 0,
"outs": output_count,
"subsidy": subsidy,
"swtotal_size": 0,
"swtotal_weight": 0,
"swtxs": 0,
"time": block.header.timestamp,
"total_out": total_out,
"total_size": block_size,
"total_weight": block_weight,
"totalfee": total_fees as f64 / 100_000_000.0,
"txs": tx_count,
"utxo_increase": 0,
"utxo_size_inc": 0
}))
} else {
Err(BlockNotFoundError::new("").into())
}
} else {
Err(anyhow::anyhow!(STORAGE_NOT_AVAILABLE_MSG))
}
}
pub async fn prune_blockchain(&self, params: &Value) -> Result<Value> {
debug!("RPC: pruneblockchain");
let height = params
.get(0)
.and_then(|p| p.as_u64())
.ok_or_else(|| anyhow::anyhow!(HEIGHT_PARAM_REQUIRED_MSG))?;
if let Some(ref storage) = self.storage {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
let is_ibd = tip_height == 0;
if height >= tip_height {
return Err(anyhow::anyhow!(
"Cannot prune to height >= tip height ({} >= {})",
height,
tip_height
));
}
if let Some(pruning_manager) = storage.pruning() {
let stats = pruning_manager.prune_to_height(height, tip_height, is_ibd)?;
storage.flush()?;
Ok(json!({
"pruned_height": height,
"blocks_pruned": stats.blocks_pruned,
"blocks_kept": stats.blocks_kept,
"headers_kept": stats.headers_kept,
"storage_freed_bytes": stats.storage_freed,
}))
} else {
Err(anyhow::anyhow!(
"Pruning is not enabled. Configure pruning in node configuration."
))
}
} else {
Err(anyhow::anyhow!(STORAGE_NOT_AVAILABLE_MSG))
}
}
pub async fn get_prune_info(&self, _params: &Value) -> Result<Value> {
debug!("RPC: getpruneinfo");
if let Some(ref storage) = self.storage {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
let is_pruning_enabled = storage.is_pruning_enabled();
if let Some(pruning_manager) = storage.pruning() {
let stats = pruning_manager.get_stats();
let config = &pruning_manager.config;
let mode_str = match &config.mode {
crate::config::PruningMode::Disabled => "disabled",
crate::config::PruningMode::Normal { .. } => "normal",
#[cfg(feature = "utxo-commitments")]
crate::config::PruningMode::Aggressive { .. } => "aggressive",
crate::config::PruningMode::Custom { .. } => "custom",
#[cfg(not(feature = "utxo-commitments"))]
_ => "unknown", };
Ok(json!({
"pruning_enabled": is_pruning_enabled,
"mode": mode_str,
"auto_prune": config.auto_prune,
"auto_prune_interval": config.auto_prune_interval,
"min_blocks_to_keep": config.min_blocks_to_keep,
"current_height": tip_height,
"last_prune_height": stats.last_prune_height,
"total_blocks_pruned": stats.blocks_pruned,
"total_blocks_kept": stats.blocks_kept,
"total_headers_kept": stats.headers_kept,
"total_storage_freed_bytes": stats.storage_freed,
}))
} else {
Ok(json!({
"pruning_enabled": false,
"mode": "disabled",
"auto_prune": false,
"current_height": tip_height,
}))
}
} else {
Ok(json!({
"pruning_enabled": false,
"mode": "disabled",
"note": STORAGE_NOT_AVAILABLE_MSG
}))
}
}
fn disconnect_invalid_block(
&self,
storage: &Storage,
target: &blvm_protocol::Hash,
) -> Result<()> {
let Some(tip) = storage.chain().get_tip_hash()? else {
return Ok(());
};
let mut chain = Vec::new();
let mut cursor = tip;
let mut found = false;
for _ in 0..100_000 {
chain.push(cursor);
if cursor == *target {
found = true;
break;
}
let Some(header) = storage.blocks().get_header(&cursor)? else {
break;
};
if header.prev_block_hash == [0u8; 32] || header.prev_block_hash == cursor {
break;
}
cursor = header.prev_block_hash;
}
if !found {
return Ok(());
}
for hash in chain {
let Some(header) = storage.blocks().get_header(&hash)? else {
break;
};
#[cfg(feature = "production")]
if let Ok(Some(undo)) = storage.blocks().get_undo_log(&hash) {
for entry in &undo.entries {
if entry.new_utxo.is_some() {
let _ = storage.utxos().remove_utxo(&entry.outpoint);
}
if let Some(prev) = &entry.previous_utxo {
let _ = storage.utxos().add_utxo(&entry.outpoint, prev.as_ref());
}
}
}
let height = storage.chain().get_height()?.unwrap_or(0);
if height == 0 {
break;
}
let parent = header.prev_block_hash;
let Some(parent_header) = storage.blocks().get_header(&parent)? else {
break;
};
storage
.chain()
.update_tip(&parent, &parent_header, height - 1)?;
if hash != *target {
let _ = storage.chain().mark_invalid(&hash);
}
}
Ok(())
}
pub async fn invalidate_block(&self, params: &Value) -> Result<Value> {
debug!("RPC: invalidateblock");
if let Some(ref protocol) = self.protocol {
if protocol.get_protocol_version() == blvm_protocol::ProtocolVersion::BitcoinV1 {
return Err(anyhow::anyhow!(
"invalidateblock is not permitted on mainnet; use regtest or testnet"
));
}
}
let blockhash = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| anyhow::anyhow!(BLOCK_HASH_PARAM_REQUIRED_MSG))?;
let hash =
decode_hash32(blockhash).map_err(|e| anyhow::anyhow!("Invalid block hash: {}", e))?;
if let Some(ref storage) = self.storage {
self.disconnect_invalid_block(storage, &hash)?;
storage.chain().mark_invalid(&hash)?;
let height = storage
.blocks()
.get_height_by_hash(&hash)
.ok()
.flatten()
.unwrap_or(0);
if let Some(ref ep) = self.event_publisher {
ep.publish_block_disconnected(&hash, height).await;
if let Ok(Some(new_tip)) = storage.chain().get_tip_hash() {
if new_tip != hash {
ep.publish_chain_reorg(&hash, &new_tip).await;
}
}
}
Ok(Value::Null)
} else {
Err(anyhow::anyhow!(STORAGE_NOT_AVAILABLE_MSG))
}
}
pub async fn reconsider_block(&self, params: &Value) -> Result<Value> {
debug!("RPC: reconsiderblock");
let blockhash = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| anyhow::anyhow!(BLOCK_HASH_PARAM_REQUIRED_MSG))?;
let hash =
decode_hash32(blockhash).map_err(|e| anyhow::anyhow!("Invalid block hash: {}", e))?;
if let Some(ref storage) = self.storage {
storage.chain().unmark_invalid(&hash)?;
Ok(Value::Null)
} else {
Err(anyhow::anyhow!(STORAGE_NOT_AVAILABLE_MSG))
}
}
pub async fn wait_for_new_block(&self, params: &Value) -> Result<Value> {
debug!("RPC: waitfornewblock");
let timeout_secs = param_u64(params, 0);
let storage = self
.require_storage()
.map_err(|e| anyhow::anyhow!("{}", e))?;
let initial_tip = storage.chain().get_tip_hash()?;
let initial_tip_clone = initial_tip;
if timeout_secs.is_none() {
if let Some(ref tip_hash) = initial_tip {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
return Ok(json!({
"hash": encode_hash32_rpc(tip_hash),
"height": tip_height
}));
}
return Err(anyhow::anyhow!("Chain not initialized"));
}
let deadline = tokio::time::Instant::now()
+ tokio::time::Duration::from_secs(timeout_secs.unwrap_or(0));
let poll_interval = POLL_INTERVAL_WAIT_FOR_BLOCK;
while tokio::time::Instant::now() < deadline {
let current = storage.chain().get_tip_hash()?;
match (&initial_tip_clone, ¤t) {
(Some(init), Some(cur)) if init != cur => {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
return Ok(json!({
"hash": encode_hash32_rpc(cur),
"height": tip_height
}));
}
(None, Some(cur)) => {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
return Ok(json!({
"hash": encode_hash32_rpc(cur),
"height": tip_height
}));
}
_ => {}
}
tokio::time::sleep(poll_interval).await;
}
if let Some(ref tip_hash) = initial_tip {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
Ok(json!({
"hash": encode_hash32_rpc(tip_hash),
"height": tip_height
}))
} else {
Err(anyhow::anyhow!("Chain not initialized"))
}
}
pub async fn wait_for_block(&self, params: &Value) -> Result<Value> {
debug!("RPC: waitforblock");
let blockhash =
param_str_required(params, 0, "waitforblock").map_err(|e| anyhow::anyhow!("{}", e))?;
let timeout_secs = param_u64(params, 1);
let hash =
decode_hash32(&blockhash).map_err(|e| anyhow::anyhow!("Invalid block hash: {}", e))?;
let storage = self
.require_storage()
.map_err(|e| anyhow::anyhow!("{}", e))?;
if timeout_secs.is_none() {
if let Ok(Some(_block)) = storage.blocks().get_block(&hash) {
let height = storage.blocks().get_height_by_hash(&hash)?.unwrap_or(0);
return Ok(json!({
"hash": blockhash,
"height": height
}));
}
return Err(BlockNotFoundError::new("").into());
}
let deadline = tokio::time::Instant::now()
+ tokio::time::Duration::from_secs(timeout_secs.unwrap_or(0));
let poll_interval = POLL_INTERVAL_WAIT_FOR_BLOCK;
while tokio::time::Instant::now() < deadline {
if let Ok(Some(_block)) = storage.blocks().get_block(&hash) {
let height = storage.blocks().get_height_by_hash(&hash)?.unwrap_or(0);
return Ok(json!({
"hash": blockhash,
"height": height
}));
}
tokio::time::sleep(poll_interval).await;
}
Err(BlockNotFoundError::new("").into())
}
pub async fn wait_for_block_height(&self, params: &Value) -> Result<Value> {
debug!("RPC: waitforblockheight");
let height = params
.get(0)
.and_then(|p| p.as_u64())
.ok_or_else(|| anyhow::anyhow!(HEIGHT_PARAM_REQUIRED_MSG))?;
let timeout_secs = param_u64(params, 1);
let storage = self
.require_storage()
.map_err(|e| anyhow::anyhow!("{}", e))?;
if timeout_secs.is_none() {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
if height <= tip_height {
if let Ok(Some(hash)) = storage.blocks().get_hash_by_height(height) {
return Ok(json!({
"hash": encode_hash32_rpc(&hash),
"height": height
}));
}
return Err(BlockNotFoundError::new(format!("at height {height}")).into());
}
return Err(anyhow::anyhow!(
"Block at height {} not yet available (tip: {})",
height,
tip_height
));
}
let deadline = tokio::time::Instant::now()
+ tokio::time::Duration::from_secs(timeout_secs.unwrap_or(0));
let poll_interval = POLL_INTERVAL_WAIT_FOR_BLOCK;
while tokio::time::Instant::now() < deadline {
let tip_height = storage.chain().get_height()?.unwrap_or(0);
if height <= tip_height {
if let Ok(Some(hash)) = storage.blocks().get_hash_by_height(height) {
return Ok(json!({
"hash": encode_hash32_rpc(&hash),
"height": height
}));
}
}
tokio::time::sleep(poll_interval).await;
}
let tip_height = storage.chain().get_height()?.unwrap_or(0);
Err(anyhow::anyhow!(
"Block at height {} not yet available (tip: {})",
height,
tip_height
))
}
pub async fn get_block_filter(&self, params: &Value) -> Result<Value> {
debug!("RPC: getblockfilter");
let blockhash = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| anyhow::anyhow!(BLOCK_HASH_PARAM_REQUIRED_MSG))?;
let filtertype = param_u64_default(params, 1, 0);
if filtertype != 0 {
return Err(anyhow::anyhow!("Only filter type 0 (Basic) is supported"));
}
let hash =
decode_hash32(blockhash).map_err(|e| anyhow::anyhow!("Invalid block hash: {}", e))?;
if let Some(ref storage) = self.storage {
if let Ok(Some(block)) = storage.blocks().get_block(&hash) {
use blvm_protocol::bip158::build_block_filter;
let previous_scripts = spent_scripts_from_undo(storage, &hash);
match build_block_filter(&block.transactions, &previous_scripts) {
Ok(filter) => {
let prev_header = previous_filter_header(storage, &block.header);
let header = chain_filter_header(&filter.filter_data, &prev_header);
let _ = storage.blocks().store_filter_header(&hash, &header);
Ok(json!({
"filter": hex::encode(&filter.filter_data),
"header": hex::encode(header),
}))
}
Err(e) => Err(anyhow::anyhow!("Failed to build filter: {}", e)),
}
} else {
Err(BlockNotFoundError::new("").into())
}
} else {
Err(anyhow::anyhow!(STORAGE_NOT_AVAILABLE_MSG))
}
}
pub async fn get_index_info(&self, _params: &Value) -> Result<Value> {
debug!("RPC: getindexinfo");
let storage = self.require_storage()?;
let index_stats = storage
.transactions()
.get_index_stats()
.map_err(|e| RpcError::internal_error(format!("Failed to get index stats: {e}")))?;
let best_block_height = storage.chain().get_height()?.unwrap_or(0);
Ok(json!({
"txindex": {
"synced": true,
"best_block_height": best_block_height,
"total_transactions": index_stats.total_transactions,
"address_index_enabled": index_stats.address_index_enabled,
"value_index_enabled": index_stats.value_index_enabled,
"indexed_addresses": index_stats.indexed_addresses,
"indexed_value_buckets": index_stats.indexed_value_buckets,
},
"basic block filter index": {
"synced": filter_index_synced(storage, best_block_height),
"best_block_height": best_block_height
}
}))
}
pub async fn getaddresstxids(&self, params: &Value) -> Result<Value> {
debug!("RPC: getaddresstxids");
let storage = self.require_storage()?;
let address = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| RpcError::missing_parameter("address", Some("string")))?;
let script_pubkey = Self::resolve_script_pubkey_param(address)?;
let transactions = storage
.transactions()
.get_transactions_by_address(&script_pubkey)
.map_err(|e| RpcError::internal_error(format!("Failed to query address: {e}")))?;
let txids: Vec<String> = transactions
.iter()
.map(|tx| {
let tx_hash = blvm_protocol::block::calculate_tx_id(tx);
encode_hash32_rpc(&tx_hash)
})
.collect();
Ok(json!(txids))
}
pub async fn getaddressbalance(&self, params: &Value) -> Result<Value> {
debug!("RPC: getaddressbalance");
let storage = self.require_storage()?;
let address = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| RpcError::missing_parameter("address", Some("string")))?;
let script_pubkey = Self::resolve_script_pubkey_param(address)?;
let transactions = storage
.transactions()
.get_transactions_by_address(&script_pubkey)
.map_err(|e| RpcError::internal_error(format!("Failed to query address: {e}")))?;
let mut balance: i64 = 0;
let mut received: i64 = 0;
let mut sent: i64 = 0;
for tx in &transactions {
use blvm_protocol::block::calculate_tx_id;
let txid = calculate_tx_id(tx);
for (idx, output) in tx.outputs.iter().enumerate() {
if output.script_pubkey == script_pubkey {
received += output.value;
let outpoint = blvm_protocol::OutPoint {
hash: txid,
index: idx as u32,
};
if storage.utxos().get_utxo(&outpoint).ok().flatten().is_some() {
balance += output.value;
} else {
sent += output.value;
}
}
}
}
Ok(json!({
"balance": balance,
"received": received,
"sent": sent,
}))
}
pub async fn get_blockchain_state(&self) -> Result<Value> {
debug!("RPC: getblockchainstate");
if let Some(ref storage) = self.storage {
let (tip_hash, height) = storage.chain().get_tip_hash_and_height()?;
let tip_header = storage.chain().get_tip_header()?.unwrap_or({
blvm_protocol::BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
}
});
let difficulty = Self::calculate_difficulty(tip_header.bits);
let chainwork = storage
.chain()
.get_chainwork(&tip_hash)?
.map(Self::format_chainwork)
.unwrap_or_else(|| ZERO_HASH_STR.to_string());
Ok(json!({
"chain": self.get_chain_name(),
"blocks": height,
"headers": height,
"bestblockhash": encode_hash32_rpc(&tip_hash),
"difficulty": difficulty,
"mediantime": tip_header.timestamp,
"verificationprogress": if height > 0 { 1.0 } else { 0.0 },
"initialblockdownload": height == 0,
"chainwork": chainwork,
"size_on_disk": storage.disk_size().unwrap_or(0),
"pruned": false,
"softforks": [],
"warnings": ""
}))
} else {
Ok(json!({
"chain": "regtest",
"blocks": 0,
"headers": 0,
"bestblockhash": ZERO_HASH_STR,
"difficulty": 1.0,
"mediantime": 0,
"verificationprogress": 0.0,
"initialblockdownload": true,
"chainwork": ZERO_HASH_STR,
"size_on_disk": 0,
"pruned": false,
"softforks": [],
"warnings": ""
}))
}
}
pub async fn validate_address(&self, params: &Value) -> Result<Value> {
debug!("RPC: validateaddress");
let address = crate::rpc::validation::validate_string_param(
params,
0,
"address",
Some(crate::rpc::validation::MAX_ADDRESS_STRING_LENGTH),
)
.map_err(anyhow::Error::new)?;
use crate::rpc::rawtx::address_string_to_script_pubkey;
use blvm_protocol::address::BitcoinAddress;
if let Ok(decoded) = BitcoinAddress::decode(&address) {
let is_witness = true;
let is_script = decoded.witness_version == 0 && decoded.witness_program.len() == 32;
let script_type = match (decoded.witness_version, decoded.witness_program.len()) {
(0, 20) => "witness_v0_keyhash",
(0, 32) => "witness_v0_scripthash",
(1, 32) => "witness_v1_taproot",
_ => "witness_unknown",
};
let script_pubkey = address_string_to_script_pubkey(&address).unwrap_or_default();
return Ok(json!({
"isvalid": true,
"address": address,
"scriptPubKey": hex::encode(&script_pubkey),
"isscript": is_script,
"iswitness": is_witness,
"witness_version": decoded.witness_version,
"witness_program": hex::encode(&decoded.witness_program),
"script_type": script_type,
}));
}
if let Ok(script_pubkey) = address_string_to_script_pubkey(&address) {
let is_p2sh = address.starts_with('3') || address.starts_with('2');
let script_type = if is_p2sh { "scripthash" } else { "pubkeyhash" };
return Ok(json!({
"isvalid": true,
"address": address,
"scriptPubKey": hex::encode(&script_pubkey),
"isscript": is_p2sh,
"iswitness": false,
"witness_version": null,
"witness_program": null,
"script_type": script_type,
}));
}
Ok(json!({
"isvalid": false,
"address": address,
}))
}
pub async fn get_address_info(&self, params: &Value) -> Result<Value> {
debug!("RPC: getaddressinfo");
let address = params
.get(0)
.and_then(|p| p.as_str())
.ok_or_else(|| RpcError::missing_parameter("address", Some("string")))?;
let script_pubkey = Self::resolve_script_pubkey_param(address)?;
if let Some(ref storage) = self.storage {
let index_on = storage
.transactions()
.get_index_stats()
.map(|stats| stats.address_index_enabled)
.unwrap_or(false);
if !index_on {
return Err(anyhow::anyhow!(
"getaddressinfo: address index is not enabled"
));
}
let transactions = storage
.transactions()
.get_transactions_by_address(&script_pubkey)
.map_err(|e| RpcError::internal_error(format!("Failed to query address: {e}")))?;
let tx_count = transactions.len();
let mut balance: i64 = 0;
let mut utxo_count = 0;
let mut received: i64 = 0;
for tx in &transactions {
use blvm_protocol::block::calculate_tx_id;
let txid = calculate_tx_id(tx);
for (idx, output) in tx.outputs.iter().enumerate() {
if output.script_pubkey.as_slice() == script_pubkey.as_slice()
&& in_money_range(output.value)
{
received += output.value;
let outpoint = blvm_protocol::OutPoint {
hash: txid,
index: idx as u32,
};
if storage.utxos().get_utxo(&outpoint).ok().flatten().is_some() {
balance += output.value;
utxo_count += 1;
}
}
}
}
let sent = storage
.transactions()
.spent_value_for_script(&script_pubkey)
.unwrap_or(0);
let script_type = crate::rpc::rawtx::script_pubkey_type(&script_pubkey);
Ok(json!({
"address": address,
"scriptPubKey": hex::encode(&script_pubkey),
"ismine": false,
"iswatchonly": false,
"isscript": false,
"iswitness": script_type.starts_with("witness"),
"witness_version": Value::Null,
"witness_program": Value::Null,
"script_type": script_type,
"pubkey": Value::Null,
"embedded": Value::Null,
"iscompressed": Value::Null,
"label": "",
"timestamp": Value::Null,
"hdkeypath": Value::Null,
"hdseedid": Value::Null,
"hdmasterfingerprint": Value::Null,
"labels": [],
"balance": balance as f64 / 100_000_000.0,
"received": received as f64 / 100_000_000.0,
"sent": sent as f64 / 100_000_000.0,
"tx_count": tx_count,
"utxo_count": utxo_count
}))
} else {
Err(anyhow::anyhow!(
"getaddressinfo: address index is not enabled"
))
}
}
}
#[cfg(test)]
mod txoutset_tests {
use super::*;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{OutPoint, UTXO};
#[tokio::test]
async fn txoutset_total_ignores_a_negative_value() {
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
storage
.utxos()
.add_utxo(
&OutPoint {
hash: [1u8; 32],
index: 0,
},
&UTXO {
value: 50_000,
script_pubkey: vec![OP_1].into(),
height: 0,
is_coinbase: false,
},
)
.unwrap();
storage
.utxos()
.add_utxo(
&OutPoint {
hash: [9u8; 32],
index: 0,
},
&UTXO {
value: -1,
script_pubkey: vec![OP_1].into(),
height: 0,
is_coinbase: false,
},
)
.unwrap();
let info = BlockchainRpc::with_dependencies(storage)
.get_txoutset_info()
.await
.unwrap();
let sats = (info["total_amount"].as_f64().unwrap() * 100_000_000.0).round() as u128;
assert_eq!(info["txouts"], 2);
assert_eq!(sats, 50_000);
}
}
#[cfg(test)]
mod block_stats_tests {
use super::*;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{
Block, BlockHeader, OutPoint, Transaction, TransactionInput, TransactionOutput,
};
fn tx_with_output(value: i64) -> Transaction {
Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1u8; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: blvm_protocol::constants::SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
}
}
#[tokio::test]
async fn block_stats_ignore_a_negative_output() {
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 0,
bits: 0,
nonce: 0,
},
transactions: vec![tx_with_output(-1), tx_with_output(50_000)].into_boxed_slice(),
};
let hash = storage.blocks().get_block_hash(&block);
storage.blocks().store_block(&block).unwrap();
let stats = BlockchainRpc::with_dependencies(storage)
.get_block_stats(&json!([crate::storage::hashing::hash_to_rpc_hex(&hash)]))
.await
.unwrap();
assert_eq!(stats["outs"], 2);
assert_eq!(stats["total_out"].as_u64(), Some(50_000));
assert_eq!(stats["totalfee"].as_f64(), Some(0.0));
}
#[cfg(feature = "production")]
#[tokio::test]
async fn one_thousand_sat_fee_is_min_max_and_median() {
use blvm_consensus::reorganization::{BlockUndoLog, UndoEntry};
use blvm_protocol::UTXO;
use std::sync::Arc;
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
let spent = OutPoint {
hash: [7u8; 32],
index: 0,
};
let spender = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: spent,
script_sig: Vec::new(),
sequence: blvm_protocol::constants::SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1_600_000_000,
bits: 0,
nonce: 0,
},
transactions: vec![tx_with_output(50_000), spender].into_boxed_slice(),
};
let hash = storage.blocks().get_block_hash(&block);
storage.blocks().store_block(&block).unwrap();
let mut undo = BlockUndoLog::new();
undo.push(UndoEntry {
outpoint: spent,
previous_utxo: Some(Arc::new(UTXO {
value: 2_000,
script_pubkey: vec![OP_1].into(),
height: 1,
is_coinbase: false,
})),
new_utxo: None,
});
storage.blocks().store_undo_log(&hash, &undo).unwrap();
let stats = BlockchainRpc::with_dependencies(storage)
.get_block_stats(&json!([crate::storage::hashing::hash_to_rpc_hex(&hash)]))
.await
.unwrap();
assert_eq!(stats["minfee"].as_f64(), Some(0.00001));
assert_eq!(stats["maxfee"].as_f64(), Some(0.00001));
assert_eq!(stats["medianfee"].as_f64(), Some(0.00001));
}
#[cfg(feature = "production")]
#[tokio::test]
async fn spent_script_is_in_the_filter_and_the_header_is_not_zero() {
use blvm_consensus::reorganization::{BlockUndoLog, UndoEntry};
use blvm_protocol::bip158::{build_block_filter, match_filter};
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
let spent = OutPoint {
hash: [9u8; 32],
index: 0,
};
let script = vec![OP_1];
let spender = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: spent,
script_sig: Vec::new(),
sequence: blvm_protocol::constants::SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1_000,
script_pubkey: vec![0x51],
}]
.into(),
lock_time: 0,
};
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1_600_000_000,
bits: 0,
nonce: 1,
},
transactions: vec![tx_with_output(50_000), spender].into_boxed_slice(),
};
let hash = storage.blocks().get_block_hash(&block);
storage.blocks().store_block(&block).unwrap();
let mut undo = BlockUndoLog::new();
undo.push(UndoEntry {
outpoint: spent,
previous_utxo: Some(std::sync::Arc::new(blvm_protocol::UTXO {
value: 2_000,
script_pubkey: script.clone().into(),
height: 1,
is_coinbase: false,
})),
new_utxo: None,
});
storage.blocks().store_undo_log(&hash, &undo).unwrap();
let result = BlockchainRpc::with_dependencies(Arc::clone(&storage))
.get_block_filter(&json!([crate::storage::hashing::hash_to_rpc_hex(&hash)]))
.await
.unwrap();
let built = build_block_filter(block.transactions.as_ref(), &[script.clone()]).unwrap();
assert!(match_filter(&built, &script));
assert_eq!(
result["filter"].as_str().unwrap(),
hex::encode(&built.filter_data)
);
assert_ne!(result["header"].as_str().unwrap(), &"0".repeat(64));
let rpc = BlockchainRpc::with_dependencies(Arc::clone(&storage));
let before = rpc.get_index_info(&json!([])).await.unwrap();
assert_eq!(
before["basic block filter index"]["synced"].as_bool(),
Some(false)
);
storage.chain().initialize(&block.header).unwrap();
storage.blocks().store_height(0, &hash).unwrap();
let after = rpc.get_index_info(&json!([])).await.unwrap();
assert_eq!(
after["basic block filter index"]["synced"].as_bool(),
Some(true)
);
}
#[cfg(feature = "production")]
#[tokio::test]
async fn invalidate_tip_moves_to_the_parent_and_drops_the_coinbase() {
use blvm_consensus::reorganization::{BlockUndoLog, UndoEntry};
use blvm_protocol::UTXO;
use blvm_protocol::block::calculate_tx_id;
use std::sync::Arc as StdArc;
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
let parent = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1_600_000_000,
bits: 0,
nonce: 1,
};
storage.chain().initialize(&parent).unwrap();
let parent_hash = storage.chain().get_tip_hash().unwrap().unwrap();
storage
.blocks()
.store_header(&parent_hash, &parent)
.unwrap();
let coinbase = tx_with_output(50_000);
let coinbase_id = calculate_tx_id(&coinbase);
let child = Block {
header: BlockHeader {
version: 1,
prev_block_hash: parent_hash,
merkle_root: [0u8; 32],
timestamp: 1_600_000_001,
bits: 0,
nonce: 2,
},
transactions: vec![coinbase].into_boxed_slice(),
};
let child_hash = storage.blocks().get_block_hash(&child);
storage.blocks().store_block(&child).unwrap();
storage
.blocks()
.store_header(&child_hash, &child.header)
.unwrap();
storage.blocks().store_height(1, &child_hash).unwrap();
let outpoint = OutPoint {
hash: coinbase_id,
index: 0,
};
let utxo = UTXO {
value: 50_000,
script_pubkey: vec![OP_1].into(),
height: 1,
is_coinbase: true,
};
storage.utxos().add_utxo(&outpoint, &utxo).unwrap();
let mut undo = BlockUndoLog::new();
undo.push(UndoEntry {
outpoint,
previous_utxo: None,
new_utxo: Some(StdArc::new(utxo)),
});
storage.blocks().store_undo_log(&child_hash, &undo).unwrap();
storage
.chain()
.update_tip(&child_hash, &child.header, 1)
.unwrap();
BlockchainRpc::with_dependencies(Arc::clone(&storage))
.invalidate_block(&json!([crate::storage::hashing::hash_to_rpc_hex(
&child_hash
)]))
.await
.unwrap();
assert_eq!(
storage.chain().get_tip_hash().unwrap().unwrap(),
parent_hash
);
assert!(storage.utxos().get_utxo(&outpoint).unwrap().is_none());
let tips = BlockchainRpc::with_dependencies(storage)
.get_chain_tips()
.await
.unwrap();
let old_tip = tips
.as_array()
.unwrap()
.iter()
.find(|tip| {
tip["hash"].as_str()
== Some(crate::storage::hashing::hash_to_rpc_hex(&child_hash).as_str())
})
.unwrap();
assert_eq!(old_tip["status"], "invalid");
}
#[tokio::test]
async fn p2wpkh_index_off_errors_and_a_full_spend_reports_both_sides() {
use blvm_protocol::block::calculate_tx_id;
let script = {
let mut s = vec![0x00, 0x14];
s.extend_from_slice(&[0x11u8; 20]);
s
};
let temp = tempfile::TempDir::new().unwrap();
let off = Arc::new(Storage::new(temp.path()).unwrap());
assert!(
BlockchainRpc::with_dependencies(off)
.get_address_info(&json!([hex::encode(&script)]))
.await
.is_err()
);
let temp_on = tempfile::TempDir::new().unwrap();
let mut indexing = crate::config::IndexingConfig::default();
indexing.enable_address_index = true;
let on = Arc::new(
Storage::with_backend_pruning_and_indexing(
temp_on.path(),
crate::storage::database::default_backend(),
None,
Some(indexing),
None,
None,
None,
)
.unwrap(),
);
let tx = tx_with_output(50_000);
let mut paying = tx;
paying.outputs[0].script_pubkey = script.clone();
paying.outputs[0].value = 50_000;
let txid = calculate_tx_id(&paying);
on.transactions()
.index_transaction(&paying, &[2u8; 32], 1, 0)
.unwrap();
on.transactions()
.note_spend(&script, 50_000, &txid)
.unwrap();
let info = BlockchainRpc::with_dependencies(on)
.get_address_info(&json!([hex::encode(&script)]))
.await
.unwrap();
assert_eq!(info["received"].as_f64(), Some(0.0005));
assert_eq!(info["sent"].as_f64(), Some(0.0005));
assert_eq!(info["script_type"], "witness_v0_keyhash");
}
#[tokio::test]
async fn signet_stays_unknown_and_segwit_follows_its_activation_height() {
use crate::storage::chainstate::ChainParams;
use blvm_protocol::{BitcoinProtocolEngine, ProtocolVersion};
let temp = tempfile::TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp.path()).unwrap());
let protocol = Arc::new(BitcoinProtocolEngine::new(ProtocolVersion::Signet).unwrap());
let header = protocol.get_network_params().genesis_block.header.clone();
let mut params = ChainParams::default();
params.network = "signet".to_string();
storage
.chain()
.initialize_with_params(&header, params)
.unwrap();
let rpc = BlockchainRpc::with_dependencies_and_protocol(
Arc::clone(&storage),
Arc::clone(&protocol),
);
let info = rpc.get_blockchain_info().await.unwrap();
assert_eq!(info["chain"], "signet");
assert_eq!(info["initialblockdownload"], true);
assert_eq!(info["verificationprogress"].as_f64(), Some(0.0));
assert_eq!(info["softforks"]["segwit"]["active"], false);
assert_eq!(info["softforks"]["segwit"]["height"], 1);
let tip = storage.chain().get_tip_hash().unwrap().unwrap();
storage.chain().update_tip(&tip, &header, 1).unwrap();
let at_activation = rpc.get_blockchain_info().await.unwrap();
assert_eq!(at_activation["chain"], "signet");
assert_eq!(at_activation["initialblockdownload"], true);
assert_eq!(at_activation["softforks"]["segwit"]["active"], true);
let bare = tempfile::TempDir::new().unwrap();
let bare_storage = Arc::new(Storage::new(bare.path()).unwrap());
let testnet4 = Arc::new(BitcoinProtocolEngine::new(ProtocolVersion::Testnet4).unwrap());
let named = BlockchainRpc::with_dependencies_and_protocol(bare_storage, testnet4)
.get_blockchain_info()
.await
.unwrap();
assert_eq!(named["chain"], "testnet4");
assert_eq!(named["initialblockdownload"], true);
}
}