use blvm_node::node::sync::SyncCoordinator;
use blvm_node::rpc::blockchain::BlockchainRpc;
use blvm_node::storage::Storage;
use blvm_protocol::ConsensusProof;
use blvm_protocol::mining::MiningResult;
use blvm_protocol::segwit::Witness;
use blvm_protocol::serialization::serialize_block_with_witnesses;
use blvm_protocol::{BitcoinProtocolEngine, Block, ProtocolVersion, UtxoSet};
use std::sync::Arc;
use tempfile::TempDir;
const MINE_ATTEMPTS: u64 = 2_000_000;
const MAIN_BLOCKS: u64 = 8;
const FORK_DIVERGE_HEIGHT: u64 = 4;
fn regtest_coinbase_script_sig(height: u64) -> Vec<u8> {
if height == 0 {
return vec![0x00, 0xff];
}
let mut n = height;
let mut height_bytes = Vec::new();
while n > 0 {
height_bytes.push((n & 0xff) as u8);
n >>= 8;
}
if height_bytes.last().is_some_and(|&b| b & 0x80 != 0) {
height_bytes.push(0x00);
}
let mut script_sig = Vec::with_capacity(1 + height_bytes.len());
script_sig.push(height_bytes.len() as u8);
script_sig.extend_from_slice(&height_bytes);
if script_sig.len() < 2 {
script_sig.push(0x00);
}
script_sig
}
fn witnesses_for(block: &Block) -> Vec<Vec<Witness>> {
block
.transactions
.iter()
.map(|tx| tx.inputs.iter().map(|_| Witness::default()).collect())
.collect()
}
fn mine_and_connect(
storage: &Arc<Storage>,
coord: &mut SyncCoordinator,
utxo: &mut UtxoSet,
protocol: &BitcoinProtocolEngine,
consensus: &ConsensusProof,
connect_height: u64,
nonce_salt: u64,
) -> anyhow::Result<(Block, Vec<u8>)> {
let prev_header = storage
.chain()
.get_tip_header()?
.ok_or_else(|| anyhow::anyhow!("missing tip header"))?;
let mut prev_headers = storage
.blocks()
.get_recent_headers(2016)
.unwrap_or_default();
if prev_headers.len() < 2 {
prev_headers = vec![prev_header.clone(), prev_header.clone()];
}
let mut coinbase_script = regtest_coinbase_script_sig(connect_height);
if nonce_salt != 0 {
coinbase_script.push((nonce_salt & 0xff) as u8);
coinbase_script.push(((nonce_salt >> 8) & 0xff) as u8);
}
let mut block = consensus.create_new_block(
utxo,
&[],
connect_height,
&prev_header,
&prev_headers,
&coinbase_script,
&vec![0x51u8],
)?;
block.header.version = 4;
let (mined, result) = consensus.mine_block(block, MINE_ATTEMPTS)?;
assert!(matches!(result, MiningResult::Success));
let witnesses = witnesses_for(&mined);
let wire = serialize_block_with_witnesses(&mined, &witnesses, true);
let accepted = coord.process_block(
storage.blocks().as_ref(),
protocol,
Some(storage),
&wire,
connect_height,
utxo,
None,
None,
)?;
assert!(accepted);
let block_hash = storage.blocks().as_ref().get_block_hash(&mined);
storage
.chain()
.update_tip(&block_hash, &mined.header, connect_height)?;
Ok((mined, wire))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn getchaintips_lists_main_and_orphan_fork() -> anyhow::Result<()> {
let protocol = Arc::new(BitcoinProtocolEngine::new(ProtocolVersion::Regtest)?);
let genesis_header = protocol.get_network_params().genesis_block.header.clone();
let consensus = ConsensusProof::new();
let dir = TempDir::new()?;
let storage = Arc::new(Storage::new(dir.path())?);
storage.chain().initialize(&genesis_header)?;
let mut utxo = UtxoSet::default();
let mut coord = SyncCoordinator::new();
for h in 0..MAIN_BLOCKS {
mine_and_connect(
&storage,
&mut coord,
&mut utxo,
protocol.as_ref(),
&consensus,
h,
0,
)?;
}
let main_tip = storage.chain().get_tip_hash()?.unwrap();
let fork_dir = TempDir::new()?;
let fork_storage = Arc::new(Storage::new(fork_dir.path())?);
fork_storage.chain().initialize(&genesis_header)?;
let mut fork_utxo = UtxoSet::default();
let mut fork_coord = SyncCoordinator::new();
for h in 0..FORK_DIVERGE_HEIGHT {
mine_and_connect(
&fork_storage,
&mut fork_coord,
&mut fork_utxo,
protocol.as_ref(),
&consensus,
h,
0,
)?;
}
const FORK_NONCE_SALT: u64 = 1_000_000;
mine_and_connect(
&fork_storage,
&mut fork_coord,
&mut fork_utxo,
protocol.as_ref(),
&consensus,
FORK_DIVERGE_HEIGHT,
FORK_NONCE_SALT,
)?;
let fork_hash = fork_storage
.blocks()
.get_hash_by_height(FORK_DIVERGE_HEIGHT)?
.unwrap();
let main_hash_at_diverge = storage
.blocks()
.get_hash_by_height(FORK_DIVERGE_HEIGHT)?
.unwrap();
assert_ne!(
fork_hash, main_hash_at_diverge,
"fork must diverge by block hash at height {FORK_DIVERGE_HEIGHT}"
);
let fork_block = fork_storage.blocks().get_block(&fork_hash)?.unwrap();
let fork_wire = serialize_block_with_witnesses(&fork_block, &witnesses_for(&fork_block), true);
let mut replay_utxo = utxo.clone();
let mut replay_coord = SyncCoordinator::new();
assert!(replay_coord.process_block(
storage.blocks().as_ref(),
protocol.as_ref(),
Some(&storage),
&fork_wire,
FORK_DIVERGE_HEIGHT,
&mut replay_utxo,
None,
None,
)?);
assert_eq!(storage.chain().get_tip_hash()?.unwrap(), main_tip);
let index_tips = storage.chain().block_index().chain_tips()?;
assert_eq!(
index_tips.len(),
2,
"main tip and orphan fork tip must both appear in block index"
);
let rpc = BlockchainRpc::with_dependencies_and_protocol(Arc::clone(&storage), protocol);
let tips = rpc.get_chain_tips().await?;
let arr = tips.as_array().expect("getchaintips array");
assert_eq!(arr.len(), 2, "RPC must expose both tips");
let statuses: Vec<&str> = arr
.iter()
.map(|t| t.get("status").and_then(|s| s.as_str()).unwrap())
.collect();
assert!(statuses.contains(&"active"));
assert!(statuses.contains(&"valid"));
let fork_rpc_hash = blvm_node::storage::hashing::hash_to_rpc_hex(&fork_hash);
let fork_entry = arr
.iter()
.find(|t| {
t.get("hash")
.and_then(|s| s.as_str())
.is_some_and(|h| h == fork_rpc_hash)
})
.expect("fork tip in getchaintips");
assert_eq!(
fork_entry.get("height").and_then(|v| v.as_u64()),
Some(FORK_DIVERGE_HEIGHT)
);
assert!(
fork_entry
.get("branchlen")
.and_then(|v| v.as_u64())
.unwrap()
> 0
);
Ok(())
}