blvm-node 0.1.31

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
//! ChainState metadata, caches, and recovery paths.

use blvm_node::storage::chainstate::{ChainInfo, ChainParams, UTXOStats};
use blvm_node::BlockHeader;
use blvm_protocol::test_utils::create_test_header;
use std::sync::Arc;

mod common;
use common::{setup_mining_chain_on, DIFFICULTY_INTERVAL};

fn seeded_storage(blocks: u64) -> (tempfile::TempDir, Arc<blvm_node::storage::Storage>) {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let storage = Arc::new(blvm_node::storage::Storage::new(temp_dir.path()).unwrap());
    setup_mining_chain_on(&storage, blocks).unwrap();
    (temp_dir, storage)
}

#[test]
fn test_store_and_load_work_and_chainwork() {
    let temp_db = common::TempDb::new().unwrap();
    let chain = &temp_db.chain_state;
    let header = create_test_header(1_700_000_000, [0x11; 32]);
    chain.initialize(&header).unwrap();
    let hash = [0x22u8; 32];
    chain.store_work(&hash, 42).unwrap();
    chain.store_chainwork(&hash, 999).unwrap();
    assert_eq!(chain.get_work(&hash).unwrap(), Some(42));
    assert_eq!(chain.get_chainwork(&hash).unwrap(), Some(999));
    assert!(chain.calculate_total_work().unwrap() >= 42);
}

#[test]
fn test_utxo_stats_and_hashrate_caches() {
    let (_dir, storage) = seeded_storage(12);
    let chain = storage.chain();
    let tip = chain.get_tip_hash().unwrap().expect("tip");
    let height = chain.get_height().unwrap().unwrap();

    let stats = UTXOStats {
        height,
        txouts: 3,
        total_amount: 150_000,
        muhash: [0x33; 32],
        transactions: 5,
    };
    chain.store_utxo_stats(&tip, &stats).unwrap();
    let loaded = chain.get_utxo_stats(&tip).unwrap().expect("stats");
    assert_eq!(loaded.txouts, 3);
    assert_eq!(
        chain.get_latest_utxo_stats().unwrap().unwrap().height,
        height
    );

    chain.store_network_hashrate(height, 1.5e12).unwrap();
    assert!(chain.get_network_hashrate().unwrap().unwrap() > 0.0);
}

#[test]
fn test_update_utxo_stats_cache_from_protocol_utxo_set() {
    use blvm_protocol::UtxoSet;
    use std::sync::Arc;

    let (_dir, storage) = seeded_storage(8);
    let chain = storage.chain();
    let tip = chain.get_tip_hash().unwrap().expect("tip");
    let height = chain.get_height().unwrap().unwrap();

    let mut utxo_set = UtxoSet::default();
    utxo_set.insert(
        blvm_protocol::OutPoint {
            hash: [0x44; 32],
            index: 0,
        },
        Arc::new(blvm_protocol::UTXO {
            value: 50_000,
            script_pubkey: vec![0x51].into(),
            height: 1,
            is_coinbase: false,
        }),
    );

    chain
        .update_utxo_stats_cache(&tip, height, &utxo_set, 10)
        .unwrap();
    let cached = chain.get_utxo_stats(&tip).unwrap().expect("cached stats");
    assert_eq!(cached.txouts, 1);
    assert_eq!(cached.transactions, 10);
}

#[test]
fn test_calculate_and_cache_network_hashrate_with_chain() {
    let (_dir, storage) = seeded_storage(DIFFICULTY_INTERVAL);
    let chain = storage.chain();
    let height = chain.get_height().unwrap().unwrap();
    chain
        .calculate_and_cache_network_hashrate(height, storage.blocks().as_ref())
        .unwrap();
    assert!(chain.get_network_hashrate().unwrap().is_some());
}

#[test]
fn test_invalid_block_mark_and_unmark() {
    let temp_db = common::TempDb::new().unwrap();
    let chain = &temp_db.chain_state;
    let hash = [0x55u8; 32];
    assert!(!chain.is_invalid(&hash).unwrap());
    chain.mark_invalid(&hash).unwrap();
    assert!(chain.is_invalid(&hash).unwrap());
    assert_eq!(chain.get_invalid_blocks().unwrap(), vec![hash]);
    chain.unmark_invalid(&hash).unwrap();
    assert!(!chain.is_invalid(&hash).unwrap());
}

#[test]
fn test_utxo_and_engine_watermarks() {
    let temp_db = common::TempDb::new().unwrap();
    let chain = &temp_db.chain_state;
    let header = create_test_header(1_700_000_001, [0x66; 32]);
    chain.initialize(&header).unwrap();

    chain.set_utxo_watermark(100).unwrap();
    assert_eq!(chain.get_utxo_watermark().unwrap(), Some(100));
    chain.force_set_ibd_utxo_watermark(200).unwrap();
    assert_eq!(chain.get_utxo_watermark().unwrap(), Some(200));

    chain.persist_engine_export_height(512).unwrap();
    assert_eq!(chain.get_engine_export_height().unwrap(), Some(512));
    chain
        .persist_engine_checkpoint_complete(
            512,
            1,
            10_000,
            &[0u8; blvm_muhash::MUHASH_RUNNING_STATE_BYTES],
        )
        .unwrap();
    assert_eq!(chain.get_engine_export_utxo_count().unwrap(), Some(10_000));
    chain.force_reset_engine_checkpoint_metadata().unwrap();
    assert_eq!(chain.get_engine_export_height().unwrap(), Some(0));
}

#[test]
fn test_recover_chain_tip_from_blockstore_without_chain_info() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let storage = blvm_node::storage::Storage::new(temp_dir.path()).unwrap();
    setup_mining_chain_on(&storage, 20).unwrap();
    storage.chain().reset().unwrap();
    assert!(storage.chain().load_chain_info().unwrap().is_none());
    storage.recover_chain_tip_from_blockstore().unwrap();
    let info = storage
        .chain()
        .load_chain_info()
        .unwrap()
        .expect("recovered");
    assert_eq!(info.height, 19);
}

#[test]
fn test_store_chain_info_roundtrip() {
    let temp_db = common::TempDb::new().unwrap();
    let chain = &temp_db.chain_state;
    let header = BlockHeader {
        version: 1,
        prev_block_hash: [0u8; 32],
        merkle_root: [0x77; 32],
        timestamp: 1_700_000_002,
        bits: 0x0f00ffff,
        nonce: 1,
    };
    chain.initialize(&header).unwrap();
    let tip = [0x88u8; 32];
    let info = ChainInfo {
        tip_hash: tip,
        tip_header: header.clone(),
        height: 5,
        total_work: 123,
        chain_params: ChainParams::default(),
    };
    chain.store_chain_info(&info).unwrap();
    let loaded = chain.load_chain_info().unwrap().expect("chain info");
    assert_eq!(loaded.height, 5);
    assert_eq!(loaded.tip_hash, tip);
}