#![cfg(feature = "utxo-commitments")]
mod tests {
use blvm_consensus::test_utils::create_test_header;
use blvm_consensus::types::{
BlockHeader, ByteString, Hash, Natural, OutPoint, Transaction, TransactionInput,
TransactionOutput, UTXO,
};
use blvm_protocol::utxo_commitments::*;
fn create_test_utxo(value: i64, height: Natural) -> UTXO {
UTXO {
value,
script_pubkey: vec![0x76, 0xa9, 0x14].into(), height,
is_coinbase: false,
}
}
fn create_test_transaction(
inputs: Vec<OutPoint>,
outputs: Vec<(i64, ByteString)>,
) -> Transaction {
Transaction {
version: 1,
inputs: inputs
.into_iter()
.map(|prevout| TransactionInput {
prevout,
script_sig: vec![],
sequence: 0xffffffff,
})
.collect(),
outputs: outputs
.into_iter()
.map(|(value, script_pubkey)| TransactionOutput {
value,
script_pubkey,
})
.collect(),
lock_time: 0,
}
}
#[test]
fn test_utxo_commitment_full_workflow() {
let mut tree = UtxoMerkleTree::new().unwrap();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = create_test_utxo(10000, 0);
tree.insert(outpoint1.clone(), utxo1.clone()).unwrap();
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = create_test_utxo(5000, 0);
tree.insert(outpoint2.clone(), utxo2.clone()).unwrap();
let block_hash = [3; 32];
let commitment = tree.generate_commitment(block_hash, 0);
assert_eq!(commitment.utxo_count, 2);
assert_eq!(commitment.total_supply, 15000);
assert_eq!(commitment.block_height, 0);
assert_eq!(commitment.block_hash, block_hash);
let verify_result = verify_supply(&commitment);
}
#[test]
fn test_spam_transaction_removes_spent_inputs() {
use blvm_consensus::types::{
OutPoint, Transaction, TransactionInput, TransactionOutput, UTXO,
};
use blvm_protocol::utxo_commitments::initial_sync::InitialSync;
use blvm_protocol::utxo_commitments::merkle_tree::UtxoMerkleTree;
use blvm_protocol::utxo_commitments::peer_consensus::ConsensusConfig;
let config = ConsensusConfig {
min_peers: 2,
consensus_threshold: 0.8,
safety_margin: 6,
..Default::default()
};
let initial_sync = InitialSync::new(config);
let mut utxo_tree = UtxoMerkleTree::new().unwrap();
let non_spam_outpoint = OutPoint {
hash: [1u8; 32],
index: 0,
};
let non_spam_utxo = UTXO {
value: 100000, script_pubkey: vec![
0x76, 0xa9, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0xac,
]
.into(), height: 100,
is_coinbase: false,
};
utxo_tree
.insert(non_spam_outpoint.clone(), non_spam_utxo.clone())
.unwrap();
assert!(utxo_tree.get(&non_spam_outpoint).unwrap().is_some());
let initial_supply = utxo_tree.total_supply();
assert_eq!(initial_supply, 100000);
let spam_tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: non_spam_outpoint.clone(),
script_sig: vec![0x51].into(), sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50000,
script_pubkey: {
let mut script = vec![0x6a].into(); script.extend(vec![0x00; 100]); script
},
}]
.into(),
lock_time: 0,
};
let (spam_summary, _root) = initial_sync
.process_filtered_block(&mut utxo_tree, 101, &[spam_tx])
.unwrap();
assert_eq!(spam_summary.filtered_count, 1);
assert!(spam_summary.by_type.ordinals > 0 || spam_summary.by_type.dust > 0);
assert!(
utxo_tree.get(&non_spam_outpoint).unwrap().is_none(),
"Spam transaction must remove spent inputs from UTXO tree"
);
let final_supply = utxo_tree.total_supply();
assert!(
final_supply < initial_supply,
"Supply must decrease when UTXO is spent, even if transaction is spam"
);
use blvm_consensus::serialization::transaction::serialize_transaction;
use sha2::{Digest, Sha256};
let serialized = serialize_transaction(&spam_tx);
let first_hash = Sha256::digest(&serialized);
let second_hash = Sha256::digest(first_hash);
let mut spam_tx_id = [0u8; 32];
spam_tx_id.copy_from_slice(&second_hash);
let spam_output_outpoint = OutPoint {
hash: spam_tx_id,
index: 0,
};
assert!(
utxo_tree.get(&spam_output_outpoint).unwrap().is_none(),
"Spam transaction outputs should not be added to UTXO tree"
);
}
#[test]
fn test_spam_filtering_integration() {
let filter = SpamFilter::new();
let transactions = vec![
create_test_transaction(
vec![OutPoint {
hash: [1; 32],
index: 0,
}],
vec![(10000, vec![0x76, 0xa9])],
),
create_test_transaction(
vec![OutPoint {
hash: [2; 32],
index: 0,
}],
vec![(100, vec![])], ),
create_test_transaction(
vec![OutPoint {
hash: [3; 32],
index: 0,
}],
vec![(1000, {
let mut script = vec![0x6a]; script.extend(vec![0x00; 100]); script
})],
),
create_test_transaction(
vec![OutPoint {
hash: [4; 32],
index: 0,
}],
vec![(5000, vec![0x76, 0xa9])],
),
];
let (filtered_txs, summary) = filter.filter_block(&transactions);
assert_eq!(filtered_txs.len(), 2);
assert_eq!(summary.filtered_count, 2);
assert!(summary.filtered_size > 0);
}
#[test]
fn test_peer_consensus_workflow() {
let config = ConsensusConfig {
shuffle_peers: false,
..Default::default()
};
let peer_consensus = PeerConsensus::new(config);
let all_peers = vec![
PeerInfo {
address: std::net::IpAddr::V4(std::net::Ipv4Addr::new(1, 1, 1, 1)),
asn: Some(1),
country: Some("US".to_string()),
implementation: Some("reference".to_string()),
subnet: 0x01010000,
},
PeerInfo {
address: std::net::IpAddr::V4(std::net::Ipv4Addr::new(2, 2, 2, 2)),
asn: Some(2),
country: Some("DE".to_string()),
implementation: Some("reference".to_string()),
subnet: 0x02020000,
},
PeerInfo {
address: std::net::IpAddr::V4(std::net::Ipv4Addr::new(3, 3, 3, 3)),
asn: Some(3),
country: Some("JP".to_string()),
implementation: Some("reference".to_string()),
subnet: 0x03030000,
},
];
let diverse_peers = peer_consensus.discover_diverse_peers(all_peers);
assert_eq!(diverse_peers.len(), 3);
let peer_tips = vec![100000, 100050, 100100];
let checkpoint = peer_consensus.determine_checkpoint_height(peer_tips);
assert!(checkpoint > 0);
assert!(checkpoint < 100000); }
#[test]
fn test_configuration_loading() {
let config_dir = std::env::temp_dir();
let config_path = config_dir.join("utxo_commitments_test_config.json");
let default_config = UtxoCommitmentsConfig::default();
default_config.to_json_file(&config_path).unwrap();
let loaded_config = UtxoCommitmentsConfig::from_json_file(&config_path).unwrap();
assert_eq!(loaded_config.sync_mode, default_config.sync_mode);
assert_eq!(
loaded_config.verification_level,
default_config.verification_level
);
assert_eq!(
loaded_config.consensus.min_peers,
default_config.consensus.min_peers
);
assert!(loaded_config.validate().is_ok());
let _ = std::fs::remove_file(&config_path);
}
#[test]
fn test_configuration_validation() {
let mut config = UtxoCommitmentsConfig::default();
assert!(config.validate().is_ok());
config.consensus.min_peers = 0;
assert!(config.validate().is_err());
config = UtxoCommitmentsConfig::default();
config.consensus.consensus_threshold = 1.5; assert!(config.validate().is_err());
config = UtxoCommitmentsConfig::default();
config.consensus.target_peers = 1; assert!(config.validate().is_err());
}
#[test]
fn test_initial_sync_with_config() {
let consensus_config = ConsensusConfig::default();
let spam_filter_config = SpamFilterConfig::default();
let initial_sync = InitialSync::with_spam_filter(consensus_config, spam_filter_config);
let mut headers = Vec::new();
let mut prev_hash = [0; 32];
for i in 0..10 {
let header = create_test_header(1234567890 + (i * 600), prev_hash);
prev_hash = compute_block_hash(&header);
headers.push(header);
}
}
#[test]
fn test_merkle_tree_incremental_updates() {
let mut tree = UtxoMerkleTree::new().unwrap();
let initial_root = tree.root();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = create_test_utxo(1000, 0);
let root1 = tree.insert(outpoint1.clone(), utxo1.clone()).unwrap();
assert_ne!(root1, initial_root);
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = create_test_utxo(2000, 0);
let root2 = tree.insert(outpoint2.clone(), utxo2.clone()).unwrap();
assert_ne!(root2, root1);
let root3 = tree.remove(&outpoint1, &utxo1).unwrap();
assert_ne!(root3, root2);
assert_eq!(tree.total_supply(), 2000);
assert_eq!(tree.utxo_count(), 1);
}
fn compute_block_hash(header: &BlockHeader) -> Hash {
use sha2::{Digest, Sha256};
let mut bytes = Vec::with_capacity(80);
bytes.extend_from_slice(&header.version.to_le_bytes());
bytes.extend_from_slice(&header.prev_block_hash);
bytes.extend_from_slice(&header.merkle_root);
bytes.extend_from_slice(&header.timestamp.to_le_bytes());
bytes.extend_from_slice(&header.bits.to_le_bytes());
bytes.extend_from_slice(&header.nonce.to_le_bytes());
let first_hash = Sha256::digest(&bytes);
let second_hash = Sha256::digest(&first_hash);
let mut hash = [0u8; 32];
hash.copy_from_slice(&second_hash);
hash
}
#[test]
fn test_utxo_set_to_merkle_tree_conversion() {
use blvm_consensus::types::UtxoSet;
use blvm_protocol::utxo_commitments::merkle_tree::UtxoMerkleTree;
let mut utxo_set = UtxoSet::default();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = create_test_utxo(10000, 100);
utxo_set.insert(outpoint1.clone(), std::sync::Arc::new(utxo1.clone()));
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = create_test_utxo(5000, 100);
utxo_set.insert(outpoint2.clone(), std::sync::Arc::new(utxo2.clone()));
let tree = UtxoMerkleTree::from_utxo_set(&utxo_set).unwrap();
assert_eq!(tree.total_supply(), 15000);
assert_eq!(tree.utxo_count(), 2);
assert!(tree.get(&outpoint1).unwrap().is_some());
assert!(tree.get(&outpoint2).unwrap().is_some());
}
#[test]
fn test_utxo_tree_incremental_update_from_utxo_set() {
use blvm_consensus::types::UtxoSet;
use blvm_protocol::utxo_commitments::merkle_tree::UtxoMerkleTree;
let mut tree = UtxoMerkleTree::new().unwrap();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = create_test_utxo(10000, 100);
tree.insert(outpoint1.clone(), utxo1.clone()).unwrap();
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = create_test_utxo(5000, 100);
tree.insert(outpoint2.clone(), utxo2.clone()).unwrap();
let initial_root = tree.root();
let initial_supply = tree.total_supply();
let mut new_utxo_set = UtxoSet::default();
let outpoint3 = OutPoint {
hash: [3; 32],
index: 0,
};
let utxo3 = create_test_utxo(8000, 101);
new_utxo_set.insert(outpoint2.clone(), std::sync::Arc::new(utxo2.clone())); new_utxo_set.insert(outpoint3.clone(), std::sync::Arc::new(utxo3.clone()));
let mut old_utxo_set = UtxoSet::default();
old_utxo_set.insert(outpoint1.clone(), std::sync::Arc::new(utxo1.clone()));
old_utxo_set.insert(outpoint2.clone(), std::sync::Arc::new(utxo2.clone()));
let new_root = tree
.update_from_utxo_set(&new_utxo_set, &old_utxo_set)
.unwrap();
assert_ne!(new_root, initial_root);
assert_eq!(tree.total_supply(), 13000); assert_eq!(tree.utxo_count(), 2);
assert!(tree.get(&outpoint1).unwrap().is_none()); assert!(tree.get(&outpoint2).unwrap().is_some()); assert!(tree.get(&outpoint3).unwrap().is_some()); }
}