Skip to main content

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")]
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")]
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")]
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")]
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}