blvm-node 0.1.29

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
//! `NodeApiIpc` wrapper — exercise `NodeAPI` trait via IPC client.

#[cfg(unix)]
#[path = "common/ipc_harness.rs"]
mod ipc_harness;

#[cfg(unix)]
#[path = "common/mod.rs"]
mod common;

#[cfg(unix)]
mod tests {
    use super::common;
    use super::ipc_harness;
    use blvm_node::module::api::events::EventManager;
    use blvm_node::module::api::node_api_ipc::NodeApiIpc;
    use blvm_node::module::api::NodeApiImpl;
    use blvm_node::module::ipc::protocol::{MessageType, RequestMessage, RequestPayload};
    use blvm_node::module::metrics::manager::{Metric, MetricValue, MetricsManager};
    use blvm_node::module::traits::NodeAPI;
    use blvm_node::network::NetworkManager;
    use blvm_node::node::mempool::MempoolManager;
    use blvm_node::node::sync::SyncCoordinator;
    use blvm_node::storage::Storage;
    use blvm_node::{Block, BlockHeader, Hash, OutPoint, UTXO};
    use common::{setup_mining_chain, valid_transaction};
    use serial_test::serial;
    use std::collections::HashMap;
    use std::sync::Arc;
    use tempfile::TempDir;
    use tokio::sync::Mutex;
    use tokio::task::JoinHandle;

    struct WrapperHarness {
        _dir: TempDir,
        _server: JoinHandle<Result<(), blvm_node::module::traits::ModuleError>>,
        api: NodeApiIpc,
        outpoint: OutPoint,
        mempool_tx_hash: Hash,
        wire_hash: Hash,
    }

    async fn wrapper_harness() -> WrapperHarness {
        let temp_dir = TempDir::new().unwrap();
        let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
        setup_mining_chain(&storage, 16).unwrap();
        let wire_block = Block {
            header: BlockHeader {
                version: 1,
                prev_block_hash: [14u8; 32],
                merkle_root: [0u8; 32],
                timestamp: 1_231_006_505 + 15 * 600,
                bits: 0x0f00ffff,
                nonce: 0,
            },
            transactions: vec![].into(),
        };
        let wire_height = 15u64;
        let wire_hash = storage.blocks().get_block_hash(&wire_block);
        storage.blocks().store_block(&wire_block).unwrap();
        storage
            .blocks()
            .store_height(wire_height, &wire_hash)
            .unwrap();
        let outpoint = OutPoint {
            hash: [0xcd; 32],
            index: 0,
        };
        storage
            .utxos()
            .add_utxo(
                &outpoint,
                &UTXO {
                    value: 50_000,
                    script_pubkey: vec![0x51].into(),
                    height: 0,
                    is_coinbase: false,
                },
            )
            .unwrap();
        let mempool = Arc::new(MempoolManager::new());
        let tx = valid_transaction();
        use blvm_protocol::block::calculate_tx_id;
        let mempool_tx_hash = calculate_tx_id(&tx);
        mempool.add_transaction(tx.clone()).unwrap();
        let tip = storage.chain().get_tip_hash().unwrap().unwrap_or([0u8; 32]);
        storage
            .transactions()
            .index_transaction(&tx, &tip, 15, 0)
            .unwrap();
        let network = Arc::new(NetworkManager::new("127.0.0.1:18460".parse().unwrap()));
        let mut node_api =
            NodeApiImpl::with_dependencies(storage, None, None, Some(mempool), Some(network));
        let coordinator = Arc::new(tokio::sync::Mutex::new(SyncCoordinator::new()));
        coordinator.lock().await.start_sync().unwrap();
        node_api.set_sync_coordinator(coordinator);
        node_api.set_metrics_manager(Arc::new(MetricsManager::new()), "cov-wrap".to_string());
        node_api.set_event_manager(Arc::new(EventManager::new()), "cov-wrap".to_string());
        let module_dir = std::path::PathBuf::from("data/modules/cov-wrap");
        std::fs::create_dir_all(&module_dir).unwrap();
        std::fs::write(module_dir.join("note.txt"), b"seed").unwrap();
        let node_api = Arc::new(node_api);

        let (_td, socket_path) = ipc_harness::setup_ipc_socket();
        let mut server = ipc_harness::spawn_ipc_server(socket_path.clone(), node_api);
        let mut client = ipc_harness::wait_bound_then_connect(&socket_path, &mut server).await;

        let id = client.next_correlation_id();
        let _ = client
            .request(RequestMessage {
                correlation_id: id,
                request_type: MessageType::Handshake,
                payload: RequestPayload::Handshake {
                    module_id: "cov-wrap".to_string(),
                    module_name: "Wrapper".to_string(),
                    version: "0.0.1".to_string(),
                },
            })
            .await;

        let api = NodeApiIpc::new(Arc::new(Mutex::new(client)), "cov-wrap".into());

        WrapperHarness {
            _dir: temp_dir,
            _server: server,
            api,
            outpoint,
            mempool_tx_hash,
            wire_hash,
        }
    }

    #[tokio::test]
    async fn wrapper_chain_mempool_and_storage_queries() {
        let h = wrapper_harness().await;
        assert!(h.api.get_block_height().await.unwrap() >= 15);
        let tip = h.api.get_chain_tip().await.unwrap();
        assert_eq!(tip.len(), 32);
        let info = h.api.get_chain_info().await.unwrap();
        assert!(info.height >= 15);
        let size = h.api.get_mempool_size().await.unwrap();
        assert!(size.transaction_count >= 1);
        let sync = h.api.get_sync_status().await.unwrap();
        assert!(sync.progress > 0.0);
        assert!(h.api.has_transaction(&h.mempool_tx_hash).await.unwrap());
        assert!(h.api.get_utxo(&h.outpoint).await.unwrap().is_some());
        assert!(h
            .api
            .check_transaction_in_mempool(&h.mempool_tx_hash)
            .await
            .unwrap());
        assert!(h
            .api
            .get_transaction(&h.mempool_tx_hash)
            .await
            .unwrap()
            .is_some());
        assert!(h.api.get_block_by_height(15).await.unwrap().is_some());
        assert!(h
            .api
            .get_mempool_transaction(&h.mempool_tx_hash)
            .await
            .unwrap()
            .is_some());
        let fee = h.api.get_fee_estimate(6).await.unwrap();
        assert!(fee <= 1_000_000);
        let txs = h.api.get_mempool_transactions().await.unwrap();
        assert!(txs.contains(&h.mempool_tx_hash));
        assert!(h.api.get_block(&h.wire_hash).await.unwrap().is_some());
        assert!(h
            .api
            .get_block_header(&h.wire_hash)
            .await
            .unwrap()
            .is_some());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn wrapper_network_lightning_and_denylist() {
        let h = wrapper_harness().await;
        let stats = h.api.get_network_stats().await.unwrap();
        assert_eq!(stats.peer_count, 0);
        let peers = h.api.get_network_peers().await.unwrap();
        assert!(peers.is_empty());
        assert!(h.api.get_lightning_node_url().await.unwrap().is_none());
        assert!(h.api.get_lightning_info().await.unwrap().is_none());

        let deny = [0x48u8; 32];
        h.api.merge_block_serve_denylist(&[deny]).await.unwrap();
        let snap = h.api.get_block_serve_denylist_snapshot().await.unwrap();
        assert!(snap.hashes.contains(&deny));
        h.api.clear_block_serve_denylist().await.unwrap();

        let tx_deny = [0x49u8; 32];
        h.api.merge_tx_serve_denylist(&[tx_deny]).await.unwrap();
        let tx_snap = h.api.get_tx_serve_denylist_snapshot().await.unwrap();
        assert!(tx_snap.hashes.contains(&tx_deny));
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn wrapper_metrics_maintenance_and_queue() {
        let h = wrapper_harness().await;
        h.api
            .report_metric(Metric {
                name: "wrap".into(),
                value: MetricValue::Gauge(1.0),
                labels: HashMap::new(),
                timestamp: 0,
            })
            .await
            .unwrap();
        let metrics = h.api.get_module_metrics("cov-wrap").await.unwrap();
        assert_eq!(metrics.len(), 1);
        let all = h.api.get_all_metrics().await.unwrap();
        assert!(!all.is_empty());

        h.api.ban_peer("127.0.0.1:8333", Some(60)).await.unwrap();
        h.api.set_block_serve_maintenance_mode(true).await.unwrap();
        h.api.set_block_serve_maintenance_mode(false).await.unwrap();
        h.api
            .queue_received_block_bytes(vec![0x01, 0x02])
            .await
            .unwrap();
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn wrapper_module_and_health_paths() {
        use blvm_node::module::ipc::protocol::EventPayload;
        use blvm_node::module::process::monitor::ModuleHealth;
        use blvm_node::module::traits::EventType;

        let h = wrapper_harness().await;
        h.api
            .report_module_health(ModuleHealth::Healthy)
            .await
            .unwrap();
        h.api
            .publish_event(
                EventType::NewBlock,
                EventPayload::NewBlock {
                    block_hash: [0x43; 32],
                    height: 8,
                },
            )
            .await
            .unwrap();
        let sync = h.api.get_sync_status().await.unwrap();
        assert!(sync.progress > 0.0);
        assert!(h.api.discover_modules().await.is_err());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn wrapper_module_info_without_manager_errors() {
        let h = wrapper_harness().await;
        assert!(h.api.get_module_info("missing").await.is_err());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn wrapper_denylist_replace_and_clear() {
        let h = wrapper_harness().await;
        let h1 = [0x51u8; 32];
        let h2 = [0x52u8; 32];
        h.api.merge_tx_serve_denylist(&[h1]).await.unwrap();
        h.api.replace_tx_serve_denylist(&[h2]).await.unwrap();
        let snap = h.api.get_tx_serve_denylist_snapshot().await.unwrap();
        assert!(snap.hashes.contains(&h2));
        h.api.clear_tx_serve_denylist().await.unwrap();
        h.api.replace_block_serve_denylist(&[h1]).await.unwrap();
        h.api.clear_block_serve_denylist().await.unwrap();
    }

    #[tokio::test]
    #[serial]
    async fn wrapper_filesystem_round_trip() {
        blvm_node::module::api::node_api::NodeApiImpl::clear_current_module_id();
        let h = wrapper_harness().await;
        let read = h.api.read_file("note.txt".into()).await.unwrap();
        assert_eq!(read, b"seed");
        h.api
            .write_file("note.txt".into(), b"via-ipc".to_vec())
            .await
            .unwrap();
        let updated = h.api.read_file("note.txt".into()).await.unwrap();
        assert_eq!(updated, b"via-ipc");
        let entries = h.api.list_directory(String::new()).await.unwrap();
        assert!(entries.iter().any(|e| e.contains("note.txt")));
        let meta = h.api.get_file_metadata("note.txt".into()).await.unwrap();
        assert!(meta.size >= 7);
        h.api.delete_file("note.txt".into()).await.unwrap();
    }

    #[tokio::test]
    async fn wrapper_get_block_template_with_long_chain() {
        let temp_dir = TempDir::new().unwrap();
        let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
        common::setup_mining_chain(&storage, 2016).unwrap();
        let mempool = Arc::new(MempoolManager::new());
        let network = Arc::new(NetworkManager::new("127.0.0.1:18472".parse().unwrap()));
        let mut node_api =
            NodeApiImpl::with_dependencies(storage, None, None, Some(mempool), Some(network));
        node_api.set_event_manager(Arc::new(EventManager::new()), "cov-wrap".into());
        let node_api = Arc::new(node_api);

        let (_td, socket_path) = ipc_harness::setup_ipc_socket();
        let mut server = ipc_harness::spawn_ipc_server(socket_path.clone(), node_api);
        let mut client = ipc_harness::wait_bound_then_connect(&socket_path, &mut server).await;

        let id = client.next_correlation_id();
        let _ = client
            .request(RequestMessage {
                correlation_id: id,
                request_type: MessageType::Handshake,
                payload: RequestPayload::Handshake {
                    module_id: "cov-wrap".to_string(),
                    module_name: "Wrapper".to_string(),
                    version: "0.0.1".to_string(),
                },
            })
            .await;
        let api = NodeApiIpc::new(Arc::new(Mutex::new(client)), "cov-wrap".into());
        let template = api
            .get_block_template(vec!["segwit".into()], None, None)
            .await
            .unwrap();
        assert!(template.height >= 2015);
        server.abort();
    }
}