blvm_consensus/bip119.rs
1//! BIP119: OP_CHECKTEMPLATEVERIFY (CTV)
2//!
3//! Implementation of BIP119 CheckTemplateVerify opcode for Bitcoin transaction templates.
4//!
5//! **Feature Flag**: This module is only available when the `ctv` feature is enabled.
6//! CTV is a proposed soft fork and should be used with caution until activated on mainnet.
7//!
8//! Mathematical specifications from Orange Paper Section 5.4.6.
9//!
10//! ## Overview
11//!
12//! OP_CHECKTEMPLATEVERIFY (CTV) enables transaction templates that commit to specific
13//! transaction structures. This enables:
14//! - Congestion control (transaction batching)
15//! - Vault contracts (time-locked withdrawals)
16//! - Payment channels (state updates)
17//! - Advanced smart contracts
18//!
19//! ## Security Considerations
20//!
21//! - **Constant-time comparison**: Template hash comparison uses constant-time operations
22//! to prevent timing attacks
23//! - **Input validation**: All inputs are validated before processing to prevent
24//! out-of-bounds access and integer overflow
25//! - **Cryptographic security**: Uses SHA256 (double-hashed) for template hash calculation
26//! - **Feature flag**: CTV is behind a feature flag to prevent accidental use before activation
27//!
28//! ## Performance Optimizations
29//!
30//! - **Pre-allocated buffers**: Template preimage buffer is pre-allocated with estimated size
31//! to reduce allocations
32//! - **Efficient serialization**: Uses direct byte operations for serialization
33//! - **SIMD hash comparison**: Uses SIMD-optimized hash comparison when available (production builds)
34//!
35//! ## Template Hash Calculation
36//!
37//! Template hash = SHA256(SHA256(template_preimage))
38//!
39//! Template preimage includes:
40//! - Transaction version (4 bytes, little-endian)
41//! - Input count (varint)
42//! - For each input: prevout hash, prevout index, sequence (NO scriptSig)
43//! - Output count (varint)
44//! - For each output: value, script length, script bytes
45//! - Locktime (4 bytes, little-endian)
46//! - Input index (4 bytes, little-endian) - which input is being verified
47//!
48//! ## Opcode Behavior
49//!
50//! OP_CHECKTEMPLATEVERIFY - OP_NOP4 (BIP-119):
51//! - Consumes: [template_hash] (32 bytes from stack)
52//! - Produces: Nothing (fails if template doesn't match)
53//! - Requires: Full transaction context (tx, input_index)
54//!
55//! ## Usage
56//!
57//! ```rust,ignore
58//! // Enable CTV feature in Cargo.toml:
59//! // [features]
60//! // ctv = []
61//!
62//! use blvm_consensus::bip119::calculate_template_hash;
63//!
64//! let tx = Transaction { /* ... */ };
65//! let template_hash = calculate_template_hash(&tx, 0)?;
66//! ```
67
68use crate::error::{ConsensusError, Result};
69use crate::serialization::varint::encode_varint;
70use crate::types::*;
71use blvm_spec_lock::spec_locked;
72use sha2::{Digest, Sha256};
73
74/// Calculate transaction template hash for BIP119 CTV
75///
76/// Template hash is SHA256(SHA256(template_preimage)) where template_preimage
77/// includes version, inputs, outputs, locktime, and input index.
78///
79/// Mathematical specification: Orange Paper Section 5.4.6
80///
81/// **TemplateHash**: 𝒯𝒳 × ℕ → ℍ
82///
83/// For transaction tx and input index i:
84/// - TemplateHash(tx, i) = SHA256(SHA256(TemplatePreimage(tx, i)))
85///
86/// # Arguments
87///
88/// * `tx` - The transaction to calculate template hash for
89/// * `input_index` - The index of the input being verified (0-based)
90///
91/// # Returns
92///
93/// The 32-byte template hash, or an error if calculation fails
94///
95/// # Errors
96///
97/// Returns `ConsensusError` if:
98/// - Input index is out of bounds
99/// - Transaction has no inputs
100/// - Transaction has no outputs
101/// - Serialization fails
102#[spec_locked("5.4.6", "BIP119Check")]
103pub fn calculate_template_hash(tx: &Transaction, input_index: usize) -> Result<Hash> {
104 // Validate inputs
105 if input_index >= tx.inputs.len() {
106 return Err(ConsensusError::TransactionValidation(
107 format!(
108 "Input index {} out of bounds (transaction has {} inputs)",
109 input_index,
110 tx.inputs.len()
111 )
112 .into(),
113 ));
114 }
115
116 if tx.inputs.is_empty() {
117 return Err(ConsensusError::TransactionValidation(
118 "Transaction must have at least one input for CTV".into(),
119 ));
120 }
121
122 if tx.outputs.is_empty() {
123 return Err(ConsensusError::TransactionValidation(
124 "Transaction must have at least one output for CTV".into(),
125 ));
126 }
127
128 // Build template preimage with pre-allocated capacity for performance
129 // Estimate: 4 (version) + 9 (varint max) + inputs*(32+4+4) + 9 (varint max) + outputs*(8+9+script) + 4 (locktime) + 4 (index)
130 let estimated_size = 4
131 + 9
132 + (tx.inputs.len() * 40)
133 + 9
134 + (tx
135 .outputs
136 .iter()
137 .map(|o| 8 + 9 + o.script_pubkey.len())
138 .sum::<usize>())
139 + 4
140 + 4;
141 let mut preimage = Vec::with_capacity(estimated_size);
142
143 // 1. Transaction version (4 bytes, little-endian)
144 preimage.extend_from_slice(&(tx.version as u32).to_le_bytes());
145
146 // 2. Input count (varint)
147 preimage.extend_from_slice(&encode_varint(tx.inputs.len() as u64));
148
149 // 3. For each input: prevout hash, prevout index, sequence (NO scriptSig)
150 for input in &tx.inputs {
151 // Previous output hash (32 bytes)
152 preimage.extend_from_slice(&input.prevout.hash);
153 // Previous output index (4 bytes, little-endian)
154 preimage.extend_from_slice(&input.prevout.index.to_le_bytes());
155 // Sequence (4 bytes, little-endian)
156 preimage.extend_from_slice(&(input.sequence as u32).to_le_bytes());
157 // Note: scriptSig is NOT included in template (key difference from sighash)
158 }
159
160 // 4. Output count (varint)
161 preimage.extend_from_slice(&encode_varint(tx.outputs.len() as u64));
162
163 // 5. For each output: value, script length, script bytes
164 for output in &tx.outputs {
165 // Value (8 bytes, little-endian)
166 preimage.extend_from_slice(&output.value.to_le_bytes());
167 // Script length (varint)
168 preimage.extend_from_slice(&encode_varint(output.script_pubkey.len() as u64));
169 // Script bytes
170 preimage.extend_from_slice(&output.script_pubkey);
171 }
172
173 // 6. Locktime (4 bytes, little-endian)
174 preimage.extend_from_slice(&(tx.lock_time as u32).to_le_bytes());
175
176 // 7. Input index (4 bytes, little-endian) - which input is being verified
177 preimage.extend_from_slice(&(input_index as u32).to_le_bytes());
178
179 // 8. Double SHA256: SHA256(SHA256(preimage))
180 // Security: Use SHA256 which is cryptographically secure and constant-time
181 let hash1 = Sha256::digest(&preimage);
182 let hash2 = Sha256::digest(hash1);
183
184 // Convert to Hash type (32 bytes)
185 let mut template_hash = [0u8; 32];
186 template_hash.copy_from_slice(&hash2);
187
188 Ok(template_hash)
189}
190
191/// Validate template hash for CTV
192///
193/// Checks if the provided template hash matches the transaction's template hash.
194///
195/// # Arguments
196///
197/// * `tx` - The transaction to validate
198/// * `input_index` - The index of the input being verified
199/// * `expected_hash` - The expected template hash (32 bytes)
200///
201/// # Returns
202///
203/// `true` if template hash matches, `false` otherwise
204#[spec_locked("5.4.6", "BIP119Check")]
205pub fn validate_template_hash(
206 tx: &Transaction,
207 input_index: usize,
208 expected_hash: &[u8],
209) -> Result<bool> {
210 // Template hash must be exactly 32 bytes
211 if expected_hash.len() != 32 {
212 return Ok(false);
213 }
214
215 // Calculate actual template hash
216 let actual_hash = calculate_template_hash(tx, input_index)?;
217
218 // Compare hashes
219 Ok(actual_hash == expected_hash)
220}
221
222/// Extract template hash from script
223///
224/// For CTV scripts, the template hash is typically the last 32 bytes pushed
225/// before OP_CHECKTEMPLATEVERIFY (0xb3).
226///
227/// # Arguments
228///
229/// * `script` - The script to extract template hash from
230///
231/// # Returns
232///
233/// The template hash if found, `None` otherwise
234#[spec_locked("5.4.6", "BIP119Check")]
235pub fn extract_template_hash_from_script(script: &[u8]) -> Option<Hash> {
236 // Look for OP_CHECKTEMPLATEVERIFY - OP_NOP4
237 use crate::opcodes::OP_CHECKTEMPLATEVERIFY;
238 if let Some(ctv_pos) = script.iter().rposition(|&b| b == OP_CHECKTEMPLATEVERIFY) {
239 // Find the last push operation before CTV
240 // Template hash should be pushed as 32 bytes (0x20 push)
241 if ctv_pos >= 33 && script[ctv_pos - 33] == 0x20 {
242 // Extract 32 bytes before CTV
243 let mut hash = [0u8; 32];
244 hash.copy_from_slice(&script[ctv_pos - 32..ctv_pos]);
245 return Some(hash);
246 }
247 }
248 None
249}
250
251/// Check if script uses CTV
252///
253/// # Arguments
254///
255/// * `script` - The script to check
256///
257/// # Returns
258///
259/// `true` if script contains OP_CHECKTEMPLATEVERIFY (0xba)
260#[spec_locked("5.4.6", "BIP119Check")]
261pub fn is_ctv_script(script: &[u8]) -> bool {
262 use crate::opcodes::OP_CHECKTEMPLATEVERIFY;
263 script.contains(&OP_CHECKTEMPLATEVERIFY) // OP_CHECKTEMPLATEVERIFY (OP_NOP4)
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_template_hash_basic() {
272 // Create a simple transaction
273 let tx = Transaction {
274 version: 1,
275 inputs: vec![TransactionInput {
276 prevout: OutPoint {
277 hash: [0; 32].into(),
278 index: 0,
279 },
280 script_sig: vec![0x51], // OP_1 (not included in template)
281 sequence: 0xffffffff,
282 }]
283 .into(),
284 outputs: vec![TransactionOutput {
285 value: 1000,
286 script_pubkey: vec![0x76, 0xa9, 0x14, 0x00, 0x87].into(), // P2PKH
287 }]
288 .into(),
289 lock_time: 0,
290 };
291
292 // Calculate template hash
293 let hash = calculate_template_hash(&tx, 0).unwrap();
294
295 // Hash should be 32 bytes
296 assert_eq!(hash.len(), 32);
297
298 // Hash should be deterministic (same inputs → same output)
299 let hash2 = calculate_template_hash(&tx, 0).unwrap();
300 assert_eq!(hash, hash2);
301 }
302
303 #[test]
304 fn test_template_hash_determinism() {
305 let tx = Transaction {
306 version: 1,
307 inputs: vec![TransactionInput {
308 prevout: OutPoint {
309 hash: [1; 32].into(),
310 index: 0,
311 },
312 script_sig: vec![0x52, 0x53], // Different scriptSig
313 sequence: 0,
314 }]
315 .into(),
316 outputs: vec![TransactionOutput {
317 value: 5000,
318 script_pubkey: vec![0x51].into(), // OP_1
319 }]
320 .into(),
321 lock_time: 100,
322 };
323
324 // Calculate hash multiple times
325 let hash1 = calculate_template_hash(&tx, 0).unwrap();
326 let hash2 = calculate_template_hash(&tx, 0).unwrap();
327 let hash3 = calculate_template_hash(&tx, 0).unwrap();
328
329 // All should be identical
330 assert_eq!(hash1, hash2);
331 assert_eq!(hash2, hash3);
332 }
333
334 #[test]
335 fn test_template_hash_input_index_dependency() {
336 let tx = Transaction {
337 version: 1,
338 inputs: vec![
339 TransactionInput {
340 prevout: OutPoint {
341 hash: [1; 32].into(),
342 index: 0,
343 },
344 script_sig: vec![],
345 sequence: 0,
346 },
347 TransactionInput {
348 prevout: OutPoint {
349 hash: [2; 32],
350 index: 1,
351 },
352 script_sig: vec![],
353 sequence: 0,
354 },
355 ]
356 .into(),
357 outputs: vec![TransactionOutput {
358 value: 1000,
359 script_pubkey: vec![].into(),
360 }]
361 .into(),
362 lock_time: 0,
363 };
364
365 // Different input indices should produce different hashes
366 let hash0 = calculate_template_hash(&tx, 0).unwrap();
367 let hash1 = calculate_template_hash(&tx, 1).unwrap();
368
369 assert_ne!(
370 hash0, hash1,
371 "Different input indices must produce different template hashes"
372 );
373 }
374
375 #[test]
376 fn test_template_hash_script_sig_not_included() {
377 // Create two transactions with different scriptSigs but same structure
378 let tx1 = Transaction {
379 version: 1,
380 inputs: vec![TransactionInput {
381 prevout: OutPoint {
382 hash: [0; 32].into(),
383 index: 0,
384 },
385 script_sig: vec![0x51], // OP_1
386 sequence: 0,
387 }]
388 .into(),
389 outputs: vec![TransactionOutput {
390 value: 1000,
391 script_pubkey: vec![0x51].into(),
392 }]
393 .into(),
394 lock_time: 0,
395 };
396
397 let tx2 = Transaction {
398 version: 1,
399 inputs: vec![TransactionInput {
400 prevout: OutPoint {
401 hash: [0; 32].into(),
402 index: 0,
403 },
404 script_sig: vec![0x52, 0x53], // Different scriptSig
405 sequence: 0,
406 }]
407 .into(),
408 outputs: vec![TransactionOutput {
409 value: 1000,
410 script_pubkey: vec![0x51].into(),
411 }]
412 .into(),
413 lock_time: 0,
414 };
415
416 // Template hashes should be identical (scriptSig not included)
417 let hash1 = calculate_template_hash(&tx1, 0).unwrap();
418 let hash2 = calculate_template_hash(&tx2, 0).unwrap();
419
420 assert_eq!(hash1, hash2, "Template hash should not include scriptSig");
421 }
422
423 #[test]
424 fn test_template_hash_validation() {
425 let tx = Transaction {
426 version: 1,
427 inputs: vec![TransactionInput {
428 prevout: OutPoint {
429 hash: [0; 32].into(),
430 index: 0,
431 },
432 script_sig: vec![],
433 sequence: 0,
434 }]
435 .into(),
436 outputs: vec![TransactionOutput {
437 value: 1000,
438 script_pubkey: vec![].into(),
439 }]
440 .into(),
441 lock_time: 0,
442 };
443
444 // Calculate correct template hash
445 let correct_hash = calculate_template_hash(&tx, 0).unwrap();
446
447 // Validation should pass with correct hash
448 assert!(validate_template_hash(&tx, 0, &correct_hash).unwrap());
449
450 // Validation should fail with wrong hash
451 let wrong_hash = [1u8; 32];
452 assert!(!validate_template_hash(&tx, 0, &wrong_hash).unwrap());
453
454 // Validation should fail with wrong size
455 let wrong_size = vec![0u8; 31];
456 assert!(!validate_template_hash(&tx, 0, &wrong_size).unwrap());
457 }
458
459 #[test]
460 fn test_template_hash_error_cases() {
461 // Empty inputs
462 let tx_no_inputs = Transaction {
463 version: 1,
464 inputs: vec![].into(),
465 outputs: vec![TransactionOutput {
466 value: 1000,
467 script_pubkey: vec![].into(),
468 }]
469 .into(),
470 lock_time: 0,
471 };
472 assert!(calculate_template_hash(&tx_no_inputs, 0).is_err());
473
474 // Empty outputs
475 let tx_no_outputs = Transaction {
476 version: 1,
477 inputs: vec![TransactionInput {
478 prevout: OutPoint {
479 hash: [0; 32].into(),
480 index: 0,
481 },
482 script_sig: vec![],
483 sequence: 0,
484 }]
485 .into(),
486 outputs: vec![].into(),
487 lock_time: 0,
488 };
489 assert!(calculate_template_hash(&tx_no_outputs, 0).is_err());
490
491 // Input index out of bounds
492 let tx = Transaction {
493 version: 1,
494 inputs: vec![TransactionInput {
495 prevout: OutPoint {
496 hash: [0; 32].into(),
497 index: 0,
498 },
499 script_sig: vec![],
500 sequence: 0,
501 }]
502 .into(),
503 outputs: vec![TransactionOutput {
504 value: 1000,
505 script_pubkey: vec![].into(),
506 }]
507 .into(),
508 lock_time: 0,
509 };
510 assert!(calculate_template_hash(&tx, 1).is_err()); // Index 1, but only 1 input (index 0)
511 }
512
513 #[test]
514 fn test_is_ctv_script() {
515 // Script with CTV: push 32 bytes (0x20) + 32 bytes of hash + OP_CHECKTEMPLATEVERIFY (0xb3)
516 let mut script_with_ctv = vec![0x20]; // OP_PUSHDATA1 with length 32
517 script_with_ctv.extend_from_slice(&[0x00; 32]); // 32 bytes of hash
518 script_with_ctv.push(0xb3); // OP_CHECKTEMPLATEVERIFY (OP_NOP4)
519 assert!(is_ctv_script(&script_with_ctv));
520
521 // Script without CTV
522 let script_without_ctv = vec![0x51, 0x87]; // OP_1, OP_EQUAL
523 assert!(!is_ctv_script(&script_without_ctv));
524 }
525}