use blvm_node::node::mempool::MempoolManager;
use blvm_node::rpc::mining::MiningRpc;
use blvm_node::storage::Storage;
use blvm_protocol::serialization::serialize_transaction;
use blvm_protocol::types::{BlockHeader, OutPoint, TransactionInput, TransactionOutput};
use blvm_protocol::Transaction;
use proptest::prelude::*;
use proptest::test_runner::Config as ProptestConfig;
use sha2::{Digest, Sha256};
use std::sync::Arc;
use tempfile::TempDir;
mod common;
use common::*;
fn transaction_strategy() -> BoxedStrategy<Transaction> {
(
any::<u64>(), prop::collection::vec(
(
any::<[u8; 32]>(), any::<u32>(), prop::collection::vec(any::<u8>(), 0..100), any::<u64>(), ),
0..10, ),
prop::collection::vec(
(
any::<i64>(), prop::collection::vec(any::<u8>(), 0..100), ),
0..10, ),
any::<u64>(), )
.prop_map(|(version, inputs, outputs, lock_time)| Transaction {
version,
inputs: inputs
.into_iter()
.map(|(hash, index, script_sig, sequence)| TransactionInput {
prevout: OutPoint {
hash: hash.into(),
index,
},
script_sig,
sequence,
})
.collect(),
outputs: outputs
.into_iter()
.map(|(value, script_pubkey)| TransactionOutput {
value,
script_pubkey,
})
.collect(),
lock_time,
})
.boxed()
}
proptest! {
#[test]
fn prop_transaction_serialization_no_panic(tx in transaction_strategy()) {
let serialized = serialize_transaction(&tx);
prop_assert!(!serialized.is_empty() || tx.inputs.is_empty() && tx.outputs.is_empty());
}
}
proptest! {
#[test]
fn prop_transaction_hash_double_sha256(tx in transaction_strategy()) {
let tx_bytes = serialize_transaction(&tx);
let hash1 = Sha256::digest(&tx_bytes);
let hash2 = Sha256::digest(hash1);
let expected_hash = {
let mut result = [0u8; 32];
result.copy_from_slice(&hash2);
hex::encode(result)
};
prop_assert_eq!(expected_hash.len(), 64);
}
}
proptest! {
#[test]
fn prop_transaction_serialization_round_trip(
version in 1i32..=2i32,
inputs_count in 0usize..=10,
outputs_count in 0usize..=10,
lock_time in 0u32..=0xffffffffu32,
) {
let inputs: Vec<TransactionInput> = (0..inputs_count)
.map(|i| TransactionInput {
prevout: OutPoint {
hash: [i as u8; 32].into(),
index: i as u32,
},
script_sig: vec![0x51; i % 100], sequence: 0xffffffff,
})
.collect();
let outputs: Vec<TransactionOutput> = (0..outputs_count)
.map(|i| TransactionOutput {
value: (i as i64) * 1000,
script_pubkey: vec![0x76; i % 50], })
.collect();
let tx = Transaction {
version: version as u64,
inputs: inputs.into(),
outputs: outputs.into(),
lock_time: lock_time as u64,
};
let serialized = serialize_transaction(&tx);
prop_assert!(!serialized.is_empty());
prop_assert!(serialized.len() >= 10);
}
}
proptest! {
#[test]
fn prop_transaction_serialization_valid(tx in transaction_strategy()) {
let serialized = serialize_transaction(&tx);
if !tx.inputs.is_empty() || !tx.outputs.is_empty() {
prop_assert!(serialized.len() >= 4);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))] #[test]
fn prop_template_height_matches(
height in 0u64..=100u64, ) {
let rt = tokio::runtime::Runtime::new().unwrap();
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
let mempool = Arc::new(MempoolManager::new());
let mining = MiningRpc::with_dependencies(storage.clone(), mempool);
let genesis_header = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
storage.chain().initialize(&genesis_header).unwrap();
let tip_hash = random_hash();
let tip_header = BlockHeader {
version: 1,
prev_block_hash: random_hash(),
merkle_root: random_hash(),
timestamp: 1231006505 + height * 600,
bits: 0x1d00ffff,
nonce: 0,
};
storage.chain().update_tip(&tip_hash, &tip_header, height).unwrap();
let params = serde_json::json!([]);
let result = rt.block_on(mining.get_block_template(¶ms));
if result.is_ok() {
let template = result.unwrap();
let template_height = template.get("height").unwrap().as_u64().unwrap();
prop_assert_eq!(template_height, height);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))] #[test]
fn prop_coinbase_value_always_positive(
height in 0u64..=100u64, ) {
let rt = tokio::runtime::Runtime::new().unwrap();
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
let mempool = Arc::new(MempoolManager::new());
let mining = MiningRpc::with_dependencies(storage.clone(), mempool);
let genesis_header = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
storage.chain().initialize(&genesis_header).unwrap();
let tip_hash = random_hash();
let tip_header = BlockHeader {
version: 1,
prev_block_hash: random_hash(),
merkle_root: random_hash(),
timestamp: 1231006505 + height * 600,
bits: 0x1d00ffff,
nonce: 0,
};
storage.chain().update_tip(&tip_hash, &tip_header, height).unwrap();
let params = serde_json::json!([]);
let result = rt.block_on(mining.get_block_template(¶ms));
if result.is_ok() {
let template = result.unwrap();
let coinbase_value = template.get("coinbasevalue").unwrap().as_u64().unwrap();
prop_assert!(coinbase_value > 0);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))] #[test]
fn prop_template_target_format(height in 0u64..=50u64) {
let rt = tokio::runtime::Runtime::new().unwrap();
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
let mempool = Arc::new(MempoolManager::new());
let mining = MiningRpc::with_dependencies(storage.clone(), mempool);
let genesis_header = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
storage.chain().initialize(&genesis_header).unwrap();
let tip_hash = random_hash();
let tip_header = BlockHeader {
version: 1,
prev_block_hash: random_hash(),
merkle_root: random_hash(),
timestamp: 1231006505 + height * 600,
bits: 0x1d00ffff,
nonce: 0,
};
storage.chain().update_tip(&tip_hash, &tip_header, height).unwrap();
let params = serde_json::json!([]);
let result = rt.block_on(mining.get_block_template(¶ms));
if result.is_ok() {
let template = result.unwrap();
let target = template.get("target").unwrap().as_str().unwrap();
prop_assert_eq!(target.len(), 64);
prop_assert!(target.chars().all(|c| c.is_ascii_hexdigit()));
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))] #[test]
fn prop_template_bits_format(height in 0u64..=50u64) {
let rt = tokio::runtime::Runtime::new().unwrap();
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
let mempool = Arc::new(MempoolManager::new());
let mining = MiningRpc::with_dependencies(storage.clone(), mempool);
let genesis_header = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
storage.chain().initialize(&genesis_header).unwrap();
let tip_hash = random_hash();
let tip_header = BlockHeader {
version: 1,
prev_block_hash: random_hash(),
merkle_root: random_hash(),
timestamp: 1231006505 + height * 600,
bits: 0x1d00ffff,
nonce: 0,
};
storage.chain().update_tip(&tip_hash, &tip_header, height).unwrap();
let params = serde_json::json!([]);
let result = rt.block_on(mining.get_block_template(¶ms));
if result.is_ok() {
let template = result.unwrap();
let bits = template.get("bits").unwrap().as_str().unwrap();
prop_assert_eq!(bits.len(), 8);
prop_assert!(bits.chars().all(|c| c.is_ascii_hexdigit()));
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))] #[test]
fn prop_rules_always_contains_csv(height in 0u64..=100u64) {
let rt = tokio::runtime::Runtime::new().unwrap();
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
let mempool = Arc::new(MempoolManager::new());
let mining = MiningRpc::with_dependencies(storage.clone(), mempool);
let genesis_header = BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 2083236893,
};
storage.chain().initialize(&genesis_header).unwrap();
let tip_hash = random_hash();
let tip_header = BlockHeader {
version: 1,
prev_block_hash: random_hash(),
merkle_root: random_hash(),
timestamp: 1231006505 + height * 600,
bits: 0x1d00ffff,
nonce: 0,
};
storage.chain().update_tip(&tip_hash, &tip_header, height).unwrap();
let params = serde_json::json!([]);
let result = rt.block_on(mining.get_block_template(¶ms));
if result.is_ok() {
let template = result.unwrap();
let rules = template.get("rules").unwrap().as_array().unwrap();
let rule_strings: Vec<String> = rules.iter()
.map(|r| r.as_str().unwrap().to_string())
.collect();
prop_assert!(rule_strings.contains(&"csv".to_string()));
}
}
}
proptest! {
#[test]
fn prop_transaction_hash_deterministic(tx in transaction_strategy()) {
let tx_bytes = serialize_transaction(&tx);
let tx_bytes_clone = tx_bytes.clone();
prop_assert_eq!(&tx_bytes, &tx_bytes_clone);
let hash1 = {
let h1 = Sha256::digest(&tx_bytes);
let h2 = Sha256::digest(h1);
hex::encode(h2)
};
let hash2 = {
let h1 = Sha256::digest(&tx_bytes_clone);
let h2 = Sha256::digest(h1);
hex::encode(h2)
};
prop_assert_eq!(hash1, hash2);
}
}
proptest! {
#[test]
fn prop_different_transactions_different_hashes(
tx1 in transaction_strategy(),
tx2 in transaction_strategy(),
) {
if tx1.version == tx2.version &&
tx1.inputs == tx2.inputs &&
tx1.outputs == tx2.outputs &&
tx1.lock_time == tx2.lock_time {
return Ok(());
}
let tx1_bytes = serialize_transaction(&tx1);
let tx2_bytes = serialize_transaction(&tx2);
let hash1 = {
let h1 = Sha256::digest(&tx1_bytes);
let h2 = Sha256::digest(h1);
hex::encode(h2)
};
let hash2 = {
let h1 = Sha256::digest(&tx2_bytes);
let h2 = Sha256::digest(h1);
hex::encode(h2)
};
prop_assert_ne!(hash1, hash2);
}
}