use crate::error::Result;
use crate::opcodes::*;
use crate::types::*;
use crate::types::{ByteString, Hash, Natural};
use crate::witness;
use bitcoin_hashes::{sha256d, Hash as BitcoinHash, HashEngine};
use blvm_spec_lock::spec_locked;
pub use crate::witness::Witness;
#[spec_locked("11.1.1")]
pub fn calculate_transaction_weight(
tx: &Transaction,
witness: Option<&Witness>,
) -> Result<Natural> {
let base_size = calculate_base_size(tx);
let total_size = calculate_total_size(tx, witness);
Ok(witness::calculate_transaction_weight_segwit(
base_size, total_size,
))
}
#[spec_locked("11.1.1")]
fn calculate_base_size(tx: &Transaction) -> Natural {
const VERSION_AND_LOCKTIME: usize = 4 + 4;
const PER_INPUT: usize = 32 + 4 + 1 + 4;
const PER_OUTPUT: usize = 8 + 1;
let n_in = tx.inputs.len();
let n_out = tx.outputs.len();
let inputs_part = n_in.saturating_mul(PER_INPUT);
let outputs_part = n_out.saturating_mul(PER_OUTPUT);
(VERSION_AND_LOCKTIME + inputs_part + outputs_part) as Natural
}
#[spec_locked("11.1.1")]
fn calculate_total_size(tx: &Transaction, witness: Option<&Witness>) -> Natural {
let base_size = calculate_base_size(tx);
if let Some(witness_data) = witness {
let witness_size: Natural = witness_data.iter().map(|w| w.len() as Natural).sum();
base_size + witness_size
} else {
base_size
}
}
#[spec_locked("11.1.4")]
pub fn compute_witness_merkle_root(block: &Block, witnesses: &[Witness]) -> Result<Hash> {
if block.transactions.is_empty() {
return Err(crate::error::ConsensusError::ConsensusRuleViolation(
"Cannot compute witness merkle root for empty block".into(),
));
}
let mut witness_hashes = Vec::new();
for (i, witness) in witnesses.iter().enumerate() {
if i == 0 {
witness_hashes.push([0u8; 32]);
} else {
let witness_hash = hash_witness(witness);
witness_hashes.push(witness_hash);
}
}
compute_merkle_root(&witness_hashes)
}
fn sha256d_bytes(data: &[u8]) -> Hash {
let result = sha256d::Hash::hash(data);
let mut hash = [0u8; 32];
hash.copy_from_slice(&result[..]);
hash
}
#[spec_locked("11.1")]
fn hash_witness(witness: &Witness) -> Hash {
let mut hasher = sha256d::Hash::engine();
for element in witness {
hasher.input(element);
}
let result = sha256d::Hash::from_engine(hasher);
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
#[spec_locked("11.1.4")]
fn hash_witness_from_nested(tx_witnesses: &[Witness]) -> Hash {
let mut hasher = sha256d::Hash::engine();
for witness_stack in tx_witnesses {
for element in witness_stack {
hasher.input(element);
}
}
let result = sha256d::Hash::from_engine(hasher);
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
hash
}
#[spec_locked("11.1.4")]
pub fn compute_witness_merkle_root_from_nested(
block: &Block,
witnesses: &[Vec<Witness>],
) -> Result<Hash> {
if block.transactions.is_empty() {
return Err(crate::error::ConsensusError::ConsensusRuleViolation(
"Cannot compute witness merkle root from empty block".into(),
));
}
let mut witness_hashes = Vec::with_capacity(block.transactions.len());
for (i, (tx, tx_witnesses)) in block.transactions.iter().zip(witnesses.iter()).enumerate() {
if i == 0 {
witness_hashes.push([0u8; 32]);
} else {
let has_witness = tx_witnesses.iter().any(|w| !w.is_empty());
let hash = if has_witness {
let serialized =
crate::serialization::transaction::serialize_transaction_with_witness(
tx,
tx_witnesses,
);
sha256d_bytes(&serialized)
} else {
let serialized = crate::serialization::transaction::serialize_transaction(tx);
sha256d_bytes(&serialized)
};
witness_hashes.push(hash);
}
}
let root = compute_merkle_root(&witness_hashes);
#[cfg(feature = "profile")]
if std::env::var("BLVM_WITNESS_DEBUG").is_ok() {
if let Ok(r) = &root {
eprintln!(
"BLVM_WITNESS_DEBUG: {} txs, root={}",
witness_hashes.len(),
hex::encode(r)
);
}
}
root
}
fn compute_merkle_root(hashes: &[Hash]) -> Result<Hash> {
crate::mining::calculate_merkle_root_from_tx_ids(hashes)
}
#[spec_locked("11.1.5")]
pub fn validate_witness_commitment(
coinbase_tx: &Transaction,
witness_merkle_root: &Hash,
coinbase_witnesses: &[Witness],
) -> Result<bool> {
let reserved_nonce: [u8; 32] = coinbase_witnesses
.first()
.and_then(|w| w.first())
.and_then(|item| item.as_slice().try_into().ok())
.unwrap_or([0u8; 32]);
let mut preimage = [0u8; 64];
preimage[..32].copy_from_slice(witness_merkle_root);
preimage[32..].copy_from_slice(&reserved_nonce);
let expected_commitment = sha256d_bytes(&preimage);
let mut last_commitment: Option<Hash> = None;
for output in &coinbase_tx.outputs {
if let Some(commitment) = extract_witness_commitment(&output.script_pubkey) {
last_commitment = Some(commitment);
}
}
match last_commitment {
Some(commitment) => {
let ok = commitment == expected_commitment;
#[cfg(feature = "profile")]
if !ok && std::env::var("BLVM_WITNESS_COMMIT_DEBUG").is_ok() {
eprintln!(
"BLVM_WITNESS_COMMIT_DEBUG: root={} nonce={} expected={} got={}",
hex::encode(witness_merkle_root),
hex::encode(reserved_nonce),
hex::encode(expected_commitment),
hex::encode(commitment),
);
}
Ok(ok)
}
None => Ok(true),
}
}
#[spec_locked("11.1.5")]
pub(crate) fn extract_witness_commitment(script: &ByteString) -> Option<Hash> {
const MAGIC: [u8; 4] = [0xaa, 0x21, 0xa9, 0xed];
if script.len() >= 38 && script[0] == OP_RETURN && script[1] == 0x24 && script[2..6] == MAGIC {
let mut commitment = [0u8; 32];
commitment.copy_from_slice(&script[6..38]);
return Some(commitment);
}
None
}
#[spec_locked("11.1.6")]
pub fn is_segwit_transaction(tx: &Transaction) -> bool {
use crate::witness::{
extract_witness_program, extract_witness_version, validate_witness_program_length,
};
tx.outputs.iter().any(|output| {
let script = &output.script_pubkey;
if let Some(version) = extract_witness_version(script) {
if let Some(program) = extract_witness_program(script, version) {
return validate_witness_program_length(&program, version);
}
}
false
})
}
#[spec_locked("11.1.1")]
pub fn calculate_block_weight(block: &Block, witnesses: &[Witness]) -> Result<Natural> {
let mut total_weight = 0;
for (i, tx) in block.transactions.iter().enumerate() {
let witness = if i < witnesses.len() {
Some(&witnesses[i])
} else {
None
};
total_weight += calculate_transaction_weight(tx, witness)?;
}
Ok(total_weight)
}
#[spec_locked("11.1.1")]
#[inline]
pub fn calculate_block_weight_from_nested(
block: &Block,
witnesses: &[Vec<Witness>],
) -> Result<Natural> {
let mut total_weight = 0;
for (i, tx) in block.transactions.iter().enumerate() {
let witness_size: Natural = if i < witnesses.len() {
witnesses[i]
.iter()
.flat_map(|w| w.iter())
.map(|e| e.len() as Natural)
.sum()
} else {
0
};
let base_size =
(4 + tx.inputs.len() * (32 + 4 + 1 + 4) + tx.outputs.len() * (8 + 1) + 4) as Natural;
total_weight +=
witness::calculate_transaction_weight_segwit(base_size, base_size + witness_size);
}
Ok(total_weight)
}
#[spec_locked("11.1.7")]
pub fn validate_segwit_block(
block: &Block,
witnesses: &[Witness],
max_block_weight: Natural,
) -> Result<bool> {
for (i, _tx) in block.transactions.iter().enumerate() {
if i < witnesses.len() && !witness::validate_segwit_witness_structure(&witnesses[i])? {
return Ok(false);
}
}
let block_weight = calculate_block_weight(block, witnesses)?;
if block_weight > max_block_weight {
return Ok(false);
}
if !block.transactions.is_empty() && !witnesses.is_empty() {
let witness_root = compute_witness_merkle_root(block, witnesses)?;
if !validate_witness_commitment(
&block.transactions[0],
&witness_root,
std::slice::from_ref(&witnesses[0]),
)? {
return Ok(false);
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::create_test_header;
#[test]
fn test_calculate_transaction_weight() {
let tx = create_test_transaction();
let witness = vec![vec![OP_1], vec![OP_2]];
let weight = calculate_transaction_weight(&tx, Some(&witness)).unwrap();
assert!(weight > 0);
}
#[test]
fn test_calculate_transaction_weight_no_witness() {
let tx = create_test_transaction();
let weight = calculate_transaction_weight(&tx, None).unwrap();
assert!(weight > 0);
}
#[test]
fn test_compute_witness_merkle_root() {
let block = create_test_block();
let witnesses = vec![
vec![], vec![vec![OP_1]], ];
let root = compute_witness_merkle_root(&block, &witnesses).unwrap();
assert_eq!(root.len(), 32);
}
#[test]
fn test_compute_witness_merkle_root_empty_block() {
let block = Block {
header: create_test_header(1231006505, [0u8; 32]),
transactions: vec![].into_boxed_slice(),
};
let witnesses = vec![];
let result = compute_witness_merkle_root(&block, &witnesses);
assert!(result.is_err());
}
#[test]
fn test_validate_witness_commitment() {
let mut coinbase_tx = create_test_transaction();
let witness_root = [1u8; 32];
let nonce = [0u8; 32];
coinbase_tx.outputs[0].script_pubkey =
create_witness_commitment_script(&witness_root, &nonce);
let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
assert!(is_valid);
}
#[test]
fn test_is_segwit_transaction() {
let mut tx = create_test_transaction();
let p2wpkh_hash = [0x51; 20]; let mut script_pubkey = vec![OP_0, PUSH_20_BYTES]; script_pubkey.extend_from_slice(&p2wpkh_hash);
tx.outputs[0].script_pubkey = script_pubkey.into();
assert!(is_segwit_transaction(&tx));
}
#[test]
fn test_calculate_block_weight() {
let block = create_test_block();
let witnesses = vec![
vec![], vec![vec![OP_1]], ];
let weight = calculate_block_weight(&block, &witnesses).unwrap();
assert!(weight > 0);
}
#[test]
fn test_validate_segwit_block() {
let block = create_test_block();
let witnesses = vec![
vec![], vec![vec![OP_1]], ];
let is_valid = validate_segwit_block(&block, &witnesses, 4_000_000).unwrap();
assert!(is_valid);
}
#[test]
fn test_validate_segwit_block_exceeds_weight() {
let block = create_test_block();
let witnesses = vec![
vec![], vec![vec![OP_1]], ];
let is_valid = validate_segwit_block(&block, &witnesses, 1).unwrap(); assert!(!is_valid);
}
#[test]
fn test_validate_segwit_block_invalid_commitment() {
let mut block = create_test_block();
let witnesses = vec![
vec![], vec![vec![OP_1]], ];
let wrong_root = [2u8; 32];
block.transactions[0].outputs[0].script_pubkey =
create_witness_commitment_script(&wrong_root, &[0u8; 32]);
let is_valid = validate_segwit_block(&block, &witnesses, 4_000_000).unwrap();
assert!(!is_valid);
}
#[test]
fn test_validate_witness_commitment_no_commitment() {
let coinbase_tx = create_test_transaction();
let witness_root = [1u8; 32];
let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
assert!(is_valid); }
#[test]
fn test_validate_witness_commitment_invalid_commitment() {
let mut coinbase_tx = create_test_transaction();
let witness_root = [1u8; 32];
let invalid_root = [2u8; 32];
coinbase_tx.outputs[0].script_pubkey =
create_witness_commitment_script(&invalid_root, &[0u8; 32]);
let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
assert!(!is_valid);
}
#[test]
fn test_extract_witness_commitment_valid() {
let witness_root = [1u8; 32];
let nonce = [0u8; 32];
let script = create_witness_commitment_script(&witness_root, &nonce);
let mut preimage = [0u8; 64];
preimage[..32].copy_from_slice(&witness_root);
preimage[32..].copy_from_slice(&nonce);
let expected = sha256d_bytes(&preimage);
let extracted = extract_witness_commitment(&script).unwrap();
assert_eq!(extracted, expected);
}
#[test]
fn test_extract_witness_commitment_invalid_script() {
let script = vec![OP_1];
let extracted = extract_witness_commitment(&script);
assert!(extracted.is_none());
}
#[test]
fn test_extract_witness_commitment_wrong_opcode() {
let mut script = vec![0x52, PUSH_36_BYTES]; script.extend_from_slice(&[1u8; 32]);
let extracted = extract_witness_commitment(&script);
assert!(extracted.is_none());
}
#[test]
fn test_extract_witness_commitment_wrong_length() {
let mut script = vec![OP_RETURN, 0x25]; script.extend_from_slice(&[1u8; 32]);
let extracted = extract_witness_commitment(&script);
assert!(extracted.is_none());
}
#[test]
fn test_hash_witness() {
let witness = vec![vec![OP_1], vec![OP_2]];
let hash = hash_witness(&witness);
assert_eq!(hash.len(), 32);
let witness2 = vec![vec![OP_3], vec![OP_4]];
let hash2 = hash_witness(&witness2);
assert_ne!(hash, hash2);
}
#[test]
fn test_hash_witness_empty() {
let witness = vec![];
let hash = hash_witness(&witness);
assert_eq!(hash.len(), 32);
}
#[test]
fn test_compute_merkle_root_single_hash() {
let hashes = vec![[1u8; 32]];
let root = compute_merkle_root(&hashes).unwrap();
assert_eq!(root, [1u8; 32]);
}
#[test]
fn test_compute_merkle_root_empty() {
let hashes = vec![];
let result = compute_merkle_root(&hashes);
assert!(result.is_err());
}
#[test]
fn test_is_segwit_transaction_false() {
let tx = create_test_transaction();
assert!(!is_segwit_transaction(&tx));
}
#[test]
fn test_calculate_base_size() {
let tx = create_test_transaction();
let base_size = calculate_base_size(&tx);
assert!(base_size > 0);
}
#[test]
fn test_calculate_total_size_with_witness() {
let tx = create_test_transaction();
let witness = vec![vec![OP_1], vec![OP_2]];
let total_size = calculate_total_size(&tx, Some(&witness));
let base_size = calculate_base_size(&tx);
assert!(total_size > base_size);
}
#[test]
fn test_calculate_total_size_without_witness() {
let tx = create_test_transaction();
let total_size = calculate_total_size(&tx, None);
let base_size = calculate_base_size(&tx);
assert_eq!(total_size, base_size);
}
fn create_test_transaction() -> Transaction {
Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![OP_1],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![OP_1].into(),
}]
.into(),
lock_time: 0,
}
}
fn create_test_block() -> Block {
Block {
header: create_test_header(1231006505, [0u8; 32]),
transactions: vec![
create_test_transaction(), create_test_transaction(), ]
.into_boxed_slice(),
}
}
fn create_witness_commitment_script(witness_root: &Hash, nonce: &[u8; 32]) -> ByteString {
let mut preimage = [0u8; 64];
preimage[..32].copy_from_slice(witness_root);
preimage[32..].copy_from_slice(nonce);
let commitment = sha256d_bytes(&preimage);
let mut script = vec![OP_RETURN, PUSH_36_BYTES]; script.extend_from_slice(&[0xaa, 0x21, 0xa9, 0xed]); script.extend_from_slice(&commitment);
script.into()
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_transaction_weight_non_negative(
tx in create_transaction_strategy(),
witness in prop::option::of(create_witness_strategy())
) {
let _weight = calculate_transaction_weight(&tx, witness.as_ref()).unwrap();
}
}
proptest! {
#[test]
fn prop_transaction_weight_formula(
tx in create_transaction_strategy(),
witness in prop::option::of(create_witness_strategy())
) {
let weight = calculate_transaction_weight(&tx, witness.as_ref()).unwrap();
let base_size = calculate_base_size(&tx);
let total_size = calculate_total_size(&tx, witness.as_ref());
let expected_weight = 3 * base_size + total_size;
assert_eq!(weight, expected_weight);
}
}
proptest! {
#[test]
fn prop_block_weight_validation_limit(
block in create_block_strategy(),
witnesses in create_witnesses_strategy(),
max_weight in 1..10_000_000u64
) {
match (calculate_block_weight(&block, &witnesses), validate_segwit_block(&block, &witnesses, max_weight as Natural)) {
(Ok(actual_weight), Ok(is_valid)) => {
if actual_weight > max_weight as Natural {
prop_assert!(!is_valid, "Block exceeding weight limit must be invalid");
}
},
(Err(_), _) | (_, Err(_)) => {
}
}
}
}
proptest! {
#[test]
fn prop_witness_commitment_deterministic(
coinbase_tx in create_transaction_strategy(),
witness_root in create_hash_strategy()
) {
let result1 = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
let result2 = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
assert_eq!(result1, result2);
}
}
proptest! {
#[test]
fn prop_witness_merkle_root_deterministic(
block in create_block_strategy(),
witnesses in create_witnesses_strategy()
) {
if !block.transactions.is_empty() {
let result1 = compute_witness_merkle_root(&block, &witnesses);
let result2 = compute_witness_merkle_root(&block, &witnesses);
assert_eq!(result1.is_ok(), result2.is_ok());
if result1.is_ok() && result2.is_ok() {
assert_eq!(result1.unwrap(), result2.unwrap());
}
}
}
}
proptest! {
#[test]
fn prop_segwit_transaction_detection(
tx in create_transaction_strategy()
) {
let is_segwit = is_segwit_transaction(&tx);
let _ = is_segwit;
}
}
proptest! {
#[test]
fn prop_witness_hashing_deterministic(
witness in create_witness_strategy()
) {
let hash1 = hash_witness(&witness);
let hash2 = hash_witness(&witness);
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 32);
}
}
proptest! {
#[test]
fn prop_merkle_root_single_hash(
hash in create_hash_strategy()
) {
let hashes = vec![hash];
let root = compute_merkle_root(&hashes).unwrap();
assert_eq!(root, hash);
}
}
#[test]
fn prop_merkle_root_empty_input() {
let hashes: Vec<Hash> = vec![];
let result = compute_merkle_root(&hashes);
assert!(result.is_err());
}
proptest! {
#[test]
fn prop_witness_commitment_extraction_deterministic(
script in prop::collection::vec(any::<u8>(), 0..100)
) {
let result1 = extract_witness_commitment(&script);
let result2 = extract_witness_commitment(&script);
assert_eq!(result1.is_some(), result2.is_some());
if result1.is_some() && result2.is_some() {
assert_eq!(result1.unwrap(), result2.unwrap());
}
}
}
proptest! {
#[test]
fn prop_base_size_monotonic(
tx1 in create_transaction_strategy(),
tx2 in create_transaction_strategy()
) {
let base_size1 = calculate_base_size(&tx1);
let base_size2 = calculate_base_size(&tx2);
assert!(base_size1 > 0);
assert!(base_size2 > 0);
}
}
proptest! {
#[test]
fn prop_total_size_with_witness_greater_than_base(
tx in create_transaction_strategy(),
witness in create_witness_strategy()
) {
let base_size = calculate_base_size(&tx);
let total_size = calculate_total_size(&tx, Some(&witness));
assert!(total_size >= base_size);
}
}
fn create_transaction_strategy() -> impl Strategy<Value = Transaction> {
(
prop::collection::vec(any::<u8>(), 0..10), prop::collection::vec(any::<u8>(), 0..10), )
.prop_map(|(input_data, output_data)| {
let mut inputs = Vec::new();
for (i, _) in input_data.iter().enumerate() {
inputs.push(TransactionInput {
prevout: OutPoint {
hash: [0; 32],
index: i as u32,
},
script_sig: vec![OP_1],
sequence: 0xffffffff,
});
}
let mut outputs = Vec::new();
for _ in output_data {
outputs.push(TransactionOutput {
value: 1000,
script_pubkey: vec![OP_1],
});
}
Transaction {
version: 1,
inputs: inputs.into(),
outputs: outputs.into(),
lock_time: 0,
}
})
}
fn create_block_strategy() -> impl Strategy<Value = Block> {
prop::collection::vec(create_transaction_strategy(), 1..5).prop_map(|transactions| Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
})
}
fn create_witness_strategy() -> impl Strategy<Value = Witness> {
prop::collection::vec(prop::collection::vec(any::<u8>(), 0..10), 0..5)
}
fn create_witnesses_strategy() -> impl Strategy<Value = Vec<Witness>> {
prop::collection::vec(create_witness_strategy(), 0..5)
}
fn create_hash_strategy() -> impl Strategy<Value = Hash> {
prop::collection::vec(any::<u8>(), 32..=32).prop_map(|bytes| {
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
hash
})
}
}