use crate::error::{ConsensusError, Result};
use crate::serialization::varint::encode_varint;
use crate::types::*;
use blvm_spec_lock::spec_locked;
use sha2::{Digest, Sha256};
#[spec_locked("5.4.6")]
pub fn calculate_template_hash(tx: &Transaction, input_index: usize) -> Result<Hash> {
if input_index >= tx.inputs.len() {
return Err(ConsensusError::TransactionValidation(
format!(
"Input index {} out of bounds (transaction has {} inputs)",
input_index,
tx.inputs.len()
)
.into(),
));
}
if tx.inputs.is_empty() {
return Err(ConsensusError::TransactionValidation(
"Transaction must have at least one input for CTV".into(),
));
}
if tx.outputs.is_empty() {
return Err(ConsensusError::TransactionValidation(
"Transaction must have at least one output for CTV".into(),
));
}
let estimated_size = 4
+ 9
+ (tx.inputs.len() * 40)
+ 9
+ (tx
.outputs
.iter()
.map(|o| 8 + 9 + o.script_pubkey.len())
.sum::<usize>())
+ 4
+ 4;
let mut preimage = Vec::with_capacity(estimated_size);
preimage.extend_from_slice(&(tx.version as u32).to_le_bytes());
preimage.extend_from_slice(&encode_varint(tx.inputs.len() as u64));
for input in &tx.inputs {
preimage.extend_from_slice(&input.prevout.hash);
preimage.extend_from_slice(&input.prevout.index.to_le_bytes());
preimage.extend_from_slice(&(input.sequence as u32).to_le_bytes());
}
preimage.extend_from_slice(&encode_varint(tx.outputs.len() as u64));
for output in &tx.outputs {
preimage.extend_from_slice(&output.value.to_le_bytes());
preimage.extend_from_slice(&encode_varint(output.script_pubkey.len() as u64));
preimage.extend_from_slice(&output.script_pubkey);
}
preimage.extend_from_slice(&(tx.lock_time as u32).to_le_bytes());
preimage.extend_from_slice(&(input_index as u32).to_le_bytes());
let hash1 = Sha256::digest(&preimage);
let hash2 = Sha256::digest(hash1);
let mut template_hash = [0u8; 32];
template_hash.copy_from_slice(&hash2);
Ok(template_hash)
}
#[spec_locked("5.4.6")]
pub fn validate_template_hash(
tx: &Transaction,
input_index: usize,
expected_hash: &[u8],
) -> Result<bool> {
if expected_hash.len() != 32 {
return Ok(false);
}
let actual_hash = calculate_template_hash(tx, input_index)?;
Ok(actual_hash == expected_hash)
}
#[spec_locked("5.4.6")]
pub fn extract_template_hash_from_script(script: &[u8]) -> Option<Hash> {
use crate::opcodes::OP_CHECKTEMPLATEVERIFY;
if let Some(ctv_pos) = script.iter().rposition(|&b| b == OP_CHECKTEMPLATEVERIFY) {
if ctv_pos >= 33 && script[ctv_pos - 33] == 0x20 {
let mut hash = [0u8; 32];
hash.copy_from_slice(&script[ctv_pos - 32..ctv_pos]);
return Some(hash);
}
}
None
}
#[spec_locked("5.4.6")]
pub fn is_ctv_script(script: &[u8]) -> bool {
use crate::opcodes::OP_CHECKTEMPLATEVERIFY;
script.contains(&OP_CHECKTEMPLATEVERIFY) }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_hash_basic() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![0x51], sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![0x76, 0xa9, 0x14, 0x00, 0x87].into(), }]
.into(),
lock_time: 0,
};
let hash = calculate_template_hash(&tx, 0).unwrap();
assert_eq!(hash.len(), 32);
let hash2 = calculate_template_hash(&tx, 0).unwrap();
assert_eq!(hash, hash2);
}
#[test]
fn test_template_hash_determinism() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![0x52, 0x53], sequence: 0,
}]
.into(),
outputs: vec![TransactionOutput {
value: 5000,
script_pubkey: vec![0x51].into(), }]
.into(),
lock_time: 100,
};
let hash1 = calculate_template_hash(&tx, 0).unwrap();
let hash2 = calculate_template_hash(&tx, 0).unwrap();
let hash3 = calculate_template_hash(&tx, 0).unwrap();
assert_eq!(hash1, hash2);
assert_eq!(hash2, hash3);
}
#[test]
fn test_template_hash_input_index_dependency() {
let tx = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0,
},
TransactionInput {
prevout: OutPoint {
hash: [2; 32],
index: 1,
},
script_sig: vec![],
sequence: 0,
},
]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
let hash0 = calculate_template_hash(&tx, 0).unwrap();
let hash1 = calculate_template_hash(&tx, 1).unwrap();
assert_ne!(
hash0, hash1,
"Different input indices must produce different template hashes"
);
}
#[test]
fn test_template_hash_script_sig_not_included() {
let tx1 = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![0x51], sequence: 0,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![0x51].into(),
}]
.into(),
lock_time: 0,
};
let tx2 = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![0x52, 0x53], sequence: 0,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![0x51].into(),
}]
.into(),
lock_time: 0,
};
let hash1 = calculate_template_hash(&tx1, 0).unwrap();
let hash2 = calculate_template_hash(&tx2, 0).unwrap();
assert_eq!(hash1, hash2, "Template hash should not include scriptSig");
}
#[test]
fn test_template_hash_validation() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
let correct_hash = calculate_template_hash(&tx, 0).unwrap();
assert!(validate_template_hash(&tx, 0, &correct_hash).unwrap());
let wrong_hash = [1u8; 32];
assert!(!validate_template_hash(&tx, 0, &wrong_hash).unwrap());
let wrong_size = vec![0u8; 31];
assert!(!validate_template_hash(&tx, 0, &wrong_size).unwrap());
}
#[test]
fn test_template_hash_error_cases() {
let tx_no_inputs = Transaction {
version: 1,
inputs: vec![].into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(calculate_template_hash(&tx_no_inputs, 0).is_err());
let tx_no_outputs = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(calculate_template_hash(&tx_no_outputs, 0).is_err());
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(calculate_template_hash(&tx, 1).is_err()); }
#[test]
fn test_is_ctv_script() {
let mut script_with_ctv = vec![0x20]; script_with_ctv.extend_from_slice(&[0x00; 32]); script_with_ctv.push(0xb3); assert!(is_ctv_script(&script_with_ctv));
let script_without_ctv = vec![0x51, 0x87]; assert!(!is_ctv_script(&script_without_ctv));
}
}