Skip to main content

blvm_consensus/
witness.rs

1//! Unified witness validation framework for SegWit (BIP141) and Taproot (BIP340/341/342)
2//!
3//! Provides shared functions for witness structure validation, weight calculation,
4//! and witness data handling that are common to both SegWit and Taproot.
5
6use crate::error::Result;
7use crate::opcodes::*;
8use crate::types::*;
9use blvm_spec_lock::spec_locked;
10
11/// Witness Data: 𝒲 = 𝕊* (stack of witness elements)
12///
13/// Re-export from primitives for backward compatibility.
14/// Witness validation logic stays in this module.
15pub use crate::types::Witness;
16
17/// Witness version for SegWit (v0) and Taproot (v1)
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WitnessVersion {
20    /// SegWit version 0
21    SegWitV0 = 0,
22    /// Taproot version 1
23    TaprootV1 = 1,
24}
25
26/// Validate witness structure for SegWit
27///
28/// BIP141: Witness must be a vector of byte strings (stack elements).
29/// Each element can be up to MAX_SCRIPT_ELEMENT_SIZE bytes.
30#[spec_locked("11.1.2", "ValidateSegWitWitnessStructure")]
31pub fn validate_segwit_witness_structure(witness: &Witness) -> Result<bool> {
32    // Check each witness element size
33    // BIP141: Each witness element can be up to 520 bytes (MAX_SCRIPT_ELEMENT_SIZE)
34    // Using 520 as the limit per Bitcoin consensus rules
35    const MAX_WITNESS_ELEMENT_SIZE: usize = 520;
36    for element in witness {
37        if element.len() > MAX_WITNESS_ELEMENT_SIZE {
38            return Ok(false);
39        }
40    }
41    Ok(true)
42}
43
44/// Validate witness structure for Taproot
45///
46/// BIP341: Taproot witness structure depends on spending path:
47/// - Key path: single signature (64 bytes)
48/// - Script path: script, control block (33 + 32n bytes), and witness items
49#[spec_locked("11.2.4", "ValidateTaprootWitnessStructure")]
50pub fn validate_taproot_witness_structure(witness: &Witness, is_script_path: bool) -> Result<bool> {
51    if witness.is_empty() {
52        return Ok(false);
53    }
54
55    if is_script_path {
56        // Script path: at least script + control block
57        if witness.len() < 2 {
58            return Ok(false);
59        }
60
61        // Control block must be at least 33 bytes (internal key + leaf version + parity)
62        let control_block = &witness[witness.len() - 1];
63        if control_block.len() < 33 {
64            return Ok(false);
65        }
66
67        // Control block size: 33 + 32n (where n is number of merkle proof levels)
68        // Must be valid multiple
69        if (control_block.len() - 33) % 32 != 0 {
70            return Ok(false);
71        }
72    } else {
73        // Key path: single Schnorr signature (64 or 65 bytes per Core CheckSchnorrSignature)
74        if witness.len() != 1 {
75            return Ok(false);
76        }
77        let len = witness[0].len();
78        if len != 64 && len != 65 {
79            return Ok(false);
80        }
81        if len == 65 && witness[0][64] == 0x00 {
82            return Ok(false);
83        }
84    }
85
86    Ok(true)
87}
88
89/// Calculate transaction weight using SegWit formula
90///
91/// BIP141: Weight(tx) = 3 × BaseSize(tx) + TotalSize(tx)
92/// BaseSize: Transaction size without witness data
93/// TotalSize: Transaction size with witness data
94///
95/// BIP141: weight = base_size * 3 + total_size; result is always ≥ total_size
96/// because the base_size term adds non-negative weight.
97#[spec_locked("11.1.1", "CalculateTransactionWeight")]
98#[blvm_spec_lock::requires(total_size >= base_size)]
99#[blvm_spec_lock::ensures(result >= total_size)]
100#[blvm_spec_lock::ensures(result >= 4 * base_size)]
101pub fn calculate_transaction_weight_segwit(base_size: Natural, total_size: Natural) -> Natural {
102    3 * base_size + total_size
103}
104
105/// Calculate virtual size (vsize) from weight
106///
107/// BIP141: vsize = ceil(weight / 4)
108/// Used for fee calculation in SegWit transactions
109///
110/// Mathematical specification:
111/// - vsize = ⌈weight / 4⌉
112///
113/// Ceiling-division invariant: result * 4 ≥ weight (vsize * 4 is always ≥ weight).
114#[spec_locked("11.1.1", "WeightToVSize")]
115#[blvm_spec_lock::ensures(result * 4 >= weight)]
116#[blvm_spec_lock::ensures(result <= weight)]
117pub fn weight_to_vsize(weight: Natural) -> Natural {
118    let result = weight.div_ceil(4);
119
120    // Runtime assertion: Verify ceiling division property
121    // vsize must be >= weight / 4 (ceiling property)
122    let weight_div_4 = weight / 4;
123    debug_assert!(
124        result >= weight_div_4,
125        "Vsize ({result}) must be >= weight / 4 ({weight_div_4})"
126    );
127
128    // Runtime assertion: vsize must be <= (weight / 4) + 1 (ceiling property)
129    // Note: When weight % 4 == 0, result == weight/4, otherwise result == (weight/4) + 1
130    let weight_div_4_plus_1 = weight_div_4 + 1;
131    debug_assert!(
132        result <= weight_div_4_plus_1,
133        "Vsize ({result}) must be <= (weight / 4) + 1 ({weight_div_4_plus_1})"
134    );
135
136    // Natural is always non-negative - no assertion needed
137
138    result
139}
140
141/// Validate witness version in scriptPubKey
142///
143/// Shared function for extracting and validating witness version
144/// from SegWit v0 (OP_0 <witness-program>) or Taproot v1 (OP_1 <witness-program>)
145#[spec_locked("11.1.3", "ExtractWitnessVersion")]
146pub fn extract_witness_version(script: &ByteString) -> Option<WitnessVersion> {
147    // BIP141 §witness_program: scriptPubKey must be exactly [version, push, program_bytes]
148    // where program_bytes is 2–40 bytes. Minimum valid length is 4 bytes (version + push + 2).
149    if script.len() < 4 {
150        return None;
151    }
152
153    // Validate the push opcode encodes a 2–40 byte program.
154    let push_opcode = script[1];
155    let program_len = push_opcode as usize; // direct push opcodes (0x02..=0x28) encode their length
156    if !(2..=40).contains(&program_len) {
157        return None;
158    }
159    // The script must be exactly version(1) + push(1) + program_bytes
160    if script.len() != 2 + program_len {
161        return None;
162    }
163
164    match script[0] {
165        OP_1 => Some(WitnessVersion::TaprootV1),
166        OP_0 => Some(WitnessVersion::SegWitV0),
167        _ => None,
168    }
169}
170
171/// Extract witness program from scriptPubKey
172///
173/// For SegWit v0: Returns bytes after OP_0 and push opcode
174/// For Taproot v1: Returns bytes after OP_1 and push opcode
175///
176/// Format: [version_opcode, push_opcode, program_bytes]
177/// Returns: program_bytes (without push opcode)
178#[spec_locked("11.1.3", "ExtractWitnessProgram")]
179pub fn extract_witness_program(
180    script: &ByteString,
181    _version: WitnessVersion,
182) -> Option<ByteString> {
183    if script.len() < 3 {
184        return None;
185    }
186
187    // Skip version opcode (1 byte) and push opcode (1 byte)
188    // The push opcode tells us how many bytes follow
189    let push_opcode = script[1];
190    let program_start = 2;
191
192    // For P2WPKH: push_opcode is PUSH_20_BYTES (push 20 bytes)
193    // For P2WSH: push_opcode is PUSH_32_BYTES (push 32 bytes)
194    // For P2TR: push_opcode is PUSH_32_BYTES (push 32 bytes)
195    // Return the program bytes (after the push opcode)
196    if script.len() < program_start + (push_opcode as usize) {
197        return None;
198    }
199
200    Some(script[program_start..program_start + (push_opcode as usize)].to_vec())
201}
202
203/// Validate witness program length
204///
205/// BIP141: SegWit v0 programs are 20 or 32 bytes (P2WPKH or P2WSH)
206/// BIP341: Taproot v1 programs are 32 bytes (P2TR)
207///
208/// Length invariant: when the function returns true, the program is exactly 20 or 32 bytes.
209/// (P2WPKH = 20, P2WSH = P2TR = 32; no other lengths are valid.)
210#[spec_locked("11.1.3", "ValidateWitnessProgramLength")]
211#[blvm_spec_lock::ensures(result == false || program.len() == 20 || program.len() == 32)]
212pub fn validate_witness_program_length(program: &ByteString, version: WitnessVersion) -> bool {
213    use crate::constants::{SEGWIT_P2WPKH_LENGTH, SEGWIT_P2WSH_LENGTH, TAPROOT_PROGRAM_LENGTH};
214
215    match version {
216        WitnessVersion::SegWitV0 => {
217            // P2WPKH: 20 bytes, P2WSH: 32 bytes
218            program.len() == SEGWIT_P2WPKH_LENGTH || program.len() == SEGWIT_P2WSH_LENGTH
219        }
220        WitnessVersion::TaprootV1 => {
221            // P2TR: 32 bytes
222            program.len() == TAPROOT_PROGRAM_LENGTH
223        }
224    }
225}
226
227/// Check if witness is empty (non-witness transaction)
228///
229/// Non-emptiness invariant: if the function returns false, the witness must have at
230/// least one stack element (len > 0).  An empty witness stack trivially returns true.
231#[spec_locked("11.1.2", "IsWitnessEmpty")]
232#[blvm_spec_lock::ensures(result == true || witness.len() > 0)]
233pub fn is_witness_empty(witness: &Witness) -> bool {
234    witness.is_empty() || witness.iter().all(|elem| elem.is_empty())
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_validate_segwit_witness_structure() {
243        let witness = vec![
244            vec![0x01; 20], // P2WPKH witness
245            vec![0x02; 72], // Signature
246        ];
247        assert!(validate_segwit_witness_structure(&witness).unwrap());
248
249        // Too large element
250        let invalid_witness = vec![vec![0x01; crate::constants::MAX_SCRIPT_ELEMENT_SIZE + 1]];
251        assert!(!validate_segwit_witness_structure(&invalid_witness).unwrap());
252    }
253
254    #[test]
255    fn test_validate_taproot_witness_structure_key_path() {
256        // Key path: single 64-byte signature
257        let witness = vec![vec![0x01; 64]];
258        assert!(validate_taproot_witness_structure(&witness, false).unwrap());
259
260        // Invalid: wrong length
261        let invalid = vec![vec![0x01; 63]];
262        assert!(!validate_taproot_witness_structure(&invalid, false).unwrap());
263
264        // Valid: 65-byte sig with explicit SIGHASH_ALL (Core accepts)
265        let with_hashtype = vec![
266            vec![0x01; 64]
267                .into_iter()
268                .chain([0x01u8])
269                .collect::<Vec<_>>(),
270        ];
271        assert!(validate_taproot_witness_structure(&with_hashtype, false).unwrap());
272
273        // Invalid: explicit SIGHASH_DEFAULT suffix
274        let invalid_hashtype = vec![
275            vec![0x01; 64]
276                .into_iter()
277                .chain([0x00u8])
278                .collect::<Vec<_>>(),
279        ];
280        assert!(!validate_taproot_witness_structure(&invalid_hashtype, false).unwrap());
281
282        // Invalid: multiple elements
283        let invalid2 = vec![vec![0x01; 64], vec![0x02; 32]];
284        assert!(!validate_taproot_witness_structure(&invalid2, false).unwrap());
285    }
286
287    #[test]
288    fn test_validate_taproot_witness_structure_script_path() {
289        // Script path: script + control block (33 bytes minimum)
290        let witness = vec![
291            vec![OP_1],    // Script
292            vec![0u8; 33], // Control block (internal key + leaf version + parity)
293        ];
294        assert!(validate_taproot_witness_structure(&witness, true).unwrap());
295
296        // Invalid: control block too small
297        let invalid = vec![vec![OP_1], vec![0u8; 32]];
298        assert!(!validate_taproot_witness_structure(&invalid, true).unwrap());
299
300        // Invalid: only one element
301        let invalid2 = vec![vec![OP_1]];
302        assert!(!validate_taproot_witness_structure(&invalid2, true).unwrap());
303    }
304
305    #[test]
306    fn test_calculate_transaction_weight_segwit() {
307        let base_size = 100;
308        let total_size = 150;
309        let weight = calculate_transaction_weight_segwit(base_size, total_size);
310        assert_eq!(weight, 3 * base_size + total_size); // BIP141
311    }
312
313    #[test]
314    fn test_weight_to_vsize() {
315        assert_eq!(weight_to_vsize(400), 100); // Exact division
316        assert_eq!(weight_to_vsize(401), 101); // Ceiling
317        assert_eq!(weight_to_vsize(403), 101); // Ceiling
318        assert_eq!(weight_to_vsize(404), 101); // Ceiling
319    }
320
321    #[test]
322    fn test_extract_witness_version() {
323        // P2WPKH: OP_0 PUSH_20 <20 bytes> — must be exactly 22 bytes (BIP141).
324        let mut segwit_script = vec![OP_0, PUSH_20_BYTES];
325        segwit_script.extend([0x01u8; 20]);
326        assert_eq!(
327            extract_witness_version(&segwit_script),
328            Some(WitnessVersion::SegWitV0)
329        );
330
331        // P2TR: OP_1 PUSH_32 <32 bytes> — must be exactly 34 bytes (BIP341).
332        let mut taproot_script = vec![OP_1, PUSH_32_BYTES];
333        taproot_script.extend([0x02u8; 32]);
334        assert_eq!(
335            extract_witness_version(&taproot_script),
336            Some(WitnessVersion::TaprootV1)
337        );
338
339        let non_witness_script = vec![OP_DUP, OP_HASH160]; // OP_DUP OP_HASH160
340        assert_eq!(extract_witness_version(&non_witness_script), None);
341    }
342
343    #[test]
344    fn test_extract_witness_program() {
345        // P2WPKH format: [OP_0, PUSH_20_BYTES, <20-byte-hash>]
346        // Where OP_0 is OP_0 (witness version), PUSH_20_BYTES is push 20 bytes, then 20 bytes of hash
347        // extract_witness_program should return just the program bytes (after push opcode)
348        // Note: 0x01 to PUSH_20_BYTES is 20 bytes (1, 2, 3, ..., 20)
349        let segwit_script = vec![
350            OP_0,
351            PUSH_20_BYTES,
352            0x01,
353            0x02,
354            0x03,
355            0x04,
356            0x05,
357            0x06,
358            0x07,
359            0x08,
360            0x09,
361            0x0a,
362            0x0b,
363            0x0c,
364            0x0d,
365            0x0e,
366            0x0f,
367            0x10,
368            0x11,
369            0x12,
370            0x13,
371            PUSH_20_BYTES,
372        ];
373        let program = extract_witness_program(&segwit_script, WitnessVersion::SegWitV0);
374        // Should return the 20 bytes after the push opcode (PUSH_20_BYTES)
375        assert_eq!(
376            program,
377            Some(vec![
378                0x01,
379                0x02,
380                0x03,
381                0x04,
382                0x05,
383                0x06,
384                0x07,
385                0x08,
386                0x09,
387                0x0a,
388                0x0b,
389                0x0c,
390                0x0d,
391                0x0e,
392                0x0f,
393                0x10,
394                0x11,
395                0x12,
396                0x13,
397                PUSH_20_BYTES
398            ])
399        );
400    }
401
402    #[test]
403    fn test_validate_witness_program_length() {
404        let p2wpkh = vec![0u8; 20]; // 20 bytes
405        assert!(validate_witness_program_length(
406            &p2wpkh,
407            WitnessVersion::SegWitV0
408        ));
409
410        let p2wsh = vec![0u8; 32]; // 32 bytes
411        assert!(validate_witness_program_length(
412            &p2wsh,
413            WitnessVersion::SegWitV0
414        ));
415
416        let p2tr = vec![0u8; 32]; // 32 bytes
417        assert!(validate_witness_program_length(
418            &p2tr,
419            WitnessVersion::TaprootV1
420        ));
421
422        let invalid = vec![0u8; 33];
423        assert!(!validate_witness_program_length(
424            &invalid,
425            WitnessVersion::SegWitV0
426        ));
427        assert!(!validate_witness_program_length(
428            &invalid,
429            WitnessVersion::TaprootV1
430        ));
431    }
432
433    #[test]
434    fn test_is_witness_empty() {
435        assert!(is_witness_empty(&vec![]));
436        assert!(is_witness_empty(&vec![vec![]]));
437        assert!(!is_witness_empty(&vec![vec![0x01]]));
438    }
439}