blvm-node 0.1.43

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
//! Equal-work sibling at height H keeps first-connected tip; child on the lagging fork wins.

use blvm_node::node::sync::SyncCoordinator;
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;
/// Shared prefix ends at height 6; siblings connect at height 7.
const PREFIX_HEIGHT: u64 = 6;
const SIBLING_HEIGHT: u64 = 7;
const CHILD_HEIGHT: u64 = 8;
const FORK_SALT: u64 = 42_000;

fn regtest_coinbase_script_sig(height: u64, fork_salt: u64) -> Vec<u8> {
    let mut script = if height == 0 {
        vec![0x00, 0xff]
    } else {
        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
    };
    if fork_salt != 0 {
        script.extend_from_slice(&fork_salt.to_le_bytes());
    }
    script
}

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,
    fork_salt: u64,
) -> anyhow::Result<Block> {
    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 coinbase_script = regtest_coinbase_script_sig(connect_height, fork_salt);
    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)?;
    storage.utxos().store_utxo_set(utxo)?;
    Ok(mined)
}

fn import_side_chain_block(
    storage: &Arc<Storage>,
    coord: &mut SyncCoordinator,
    utxo: &mut UtxoSet,
    protocol: &BitcoinProtocolEngine,
    block: &Block,
    connect_height: u64,
) -> anyhow::Result<()> {
    let wire = serialize_block_with_witnesses(block, &witnesses_for(block), true);
    let accepted = coord.process_block(
        storage.blocks().as_ref(),
        protocol,
        Some(storage),
        &wire,
        connect_height,
        utxo,
        None,
        None,
    )?;
    assert!(accepted, "side-chain block at height {connect_height}");
    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn equal_work_sibling_keeps_first_tip_child_on_lagging_fork_wins() -> anyhow::Result<()> {
    let protocol = BitcoinProtocolEngine::new(ProtocolVersion::Regtest)?;
    let genesis_header = protocol.get_network_params().genesis_block.header.clone();
    let consensus = ConsensusProof::new();

    let main_dir = TempDir::new()?;
    let fork_dir = TempDir::new()?;
    let storage_main = Arc::new(Storage::new(main_dir.path())?);
    let storage_fork = Arc::new(Storage::new(fork_dir.path())?);
    storage_main.chain().initialize(&genesis_header)?;
    storage_fork.chain().initialize(&genesis_header)?;

    let mut main_utxo = UtxoSet::default();
    let mut main_coord = SyncCoordinator::new();
    for h in 0..=PREFIX_HEIGHT {
        mine_and_connect(
            &storage_main,
            &mut main_coord,
            &mut main_utxo,
            &protocol,
            &consensus,
            h,
            0,
        )?;
    }

    let block_a = mine_and_connect(
        &storage_main,
        &mut main_coord,
        &mut main_utxo,
        &protocol,
        &consensus,
        SIBLING_HEIGHT,
        0,
    )?;
    let hash_a = storage_main.blocks().get_block_hash(&block_a);
    let work_a = storage_main.chain().get_chainwork(&hash_a)?.unwrap_or(0);

    let mut fork_utxo = UtxoSet::default();
    let mut fork_coord = SyncCoordinator::new();
    for h in 0..=PREFIX_HEIGHT {
        mine_and_connect(
            &storage_fork,
            &mut fork_coord,
            &mut fork_utxo,
            &protocol,
            &consensus,
            h,
            0,
        )?;
    }

    let block_b = mine_and_connect(
        &storage_fork,
        &mut fork_coord,
        &mut fork_utxo,
        &protocol,
        &consensus,
        SIBLING_HEIGHT,
        FORK_SALT,
    )?;
    let hash_b = storage_fork.blocks().get_block_hash(&block_b);
    let work_b = storage_fork.chain().get_chainwork(&hash_b)?.unwrap_or(0);
    assert_eq!(
        work_a, work_b,
        "siblings must carry equal cumulative chainwork"
    );
    assert_ne!(hash_a, hash_b);

    let block_c = mine_and_connect(
        &storage_fork,
        &mut fork_coord,
        &mut fork_utxo,
        &protocol,
        &consensus,
        CHILD_HEIGHT,
        FORK_SALT,
    )?;
    let hash_c = storage_fork.blocks().get_block_hash(&block_c);

    let mut replay_utxo = main_utxo.clone();
    let mut replay_coord = SyncCoordinator::new();

    import_side_chain_block(
        &storage_main,
        &mut replay_coord,
        &mut replay_utxo,
        &protocol,
        &block_b,
        SIBLING_HEIGHT,
    )?;

    assert_eq!(
        storage_main.chain().get_tip_hash()?.unwrap(),
        hash_a,
        "equal-work sibling must not displace the first-connected tip"
    );
    let seq_a = storage_main
        .chain()
        .block_index()
        .get(&hash_a)?
        .unwrap()
        .sequence_id;
    let seq_b = storage_main
        .chain()
        .block_index()
        .get(&hash_b)?
        .unwrap()
        .sequence_id;
    assert!(seq_a < seq_b);

    import_side_chain_block(
        &storage_main,
        &mut replay_coord,
        &mut replay_utxo,
        &protocol,
        &block_c,
        CHILD_HEIGHT,
    )?;

    assert_eq!(
        storage_main.chain().get_tip_hash()?.unwrap(),
        hash_c,
        "heavier child on the lagging fork must become the active tip"
    );

    Ok(())
}