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 BIP340)
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() < 4 {
184        return None;
185    }
186
187    let push_opcode = script[1];
188    // BIP141 witness programs use direct push opcodes only (0x02..=0x28).
189    if !(0x02..=0x28).contains(&push_opcode) {
190        return None;
191    }
192    let program_len = push_opcode as usize;
193    if script.len() != 2 + program_len {
194        return None;
195    }
196
197    Some(script[2..2 + program_len].to_vec())
198}
199
200/// Validate witness program length
201///
202/// BIP141: SegWit v0 programs are 20 or 32 bytes (P2WPKH or P2WSH)
203/// BIP341: Taproot v1 programs are 32 bytes (P2TR)
204///
205/// Length invariant: when the function returns true, the program is exactly 20 or 32 bytes.
206/// (P2WPKH = 20, P2WSH = P2TR = 32; no other lengths are valid.)
207#[spec_locked("11.1.3", "ValidateWitnessProgramLength")]
208#[blvm_spec_lock::ensures(result == false || program.len() == 20 || program.len() == 32)]
209pub fn validate_witness_program_length(program: &ByteString, version: WitnessVersion) -> bool {
210    use crate::constants::{SEGWIT_P2WPKH_LENGTH, SEGWIT_P2WSH_LENGTH, TAPROOT_PROGRAM_LENGTH};
211
212    match version {
213        WitnessVersion::SegWitV0 => {
214            // P2WPKH: 20 bytes, P2WSH: 32 bytes
215            program.len() == SEGWIT_P2WPKH_LENGTH || program.len() == SEGWIT_P2WSH_LENGTH
216        }
217        WitnessVersion::TaprootV1 => {
218            // P2TR: 32 bytes
219            program.len() == TAPROOT_PROGRAM_LENGTH
220        }
221    }
222}
223
224/// Check if witness is empty (non-witness transaction)
225///
226/// Non-emptiness invariant: if the function returns false, the witness must have at
227/// least one stack element (len > 0).  An empty witness stack trivially returns true.
228#[spec_locked("11.1.2", "IsWitnessEmpty")]
229#[blvm_spec_lock::ensures(result == true || witness.len() > 0)]
230pub fn is_witness_empty(witness: &Witness) -> bool {
231    witness.is_empty() || witness.iter().all(|elem| elem.is_empty())
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_validate_segwit_witness_structure() {
240        let witness = vec![
241            vec![0x01; 20], // P2WPKH witness
242            vec![0x02; 72], // Signature
243        ];
244        assert!(validate_segwit_witness_structure(&witness).unwrap());
245
246        // Too large element
247        let invalid_witness = vec![vec![0x01; crate::constants::MAX_SCRIPT_ELEMENT_SIZE + 1]];
248        assert!(!validate_segwit_witness_structure(&invalid_witness).unwrap());
249    }
250
251    #[test]
252    fn test_validate_taproot_witness_structure_key_path() {
253        // Key path: single 64-byte signature
254        let witness = vec![vec![0x01; 64]];
255        assert!(validate_taproot_witness_structure(&witness, false).unwrap());
256
257        // Invalid: wrong length
258        let invalid = vec![vec![0x01; 63]];
259        assert!(!validate_taproot_witness_structure(&invalid, false).unwrap());
260
261        // Valid: 65-byte sig with explicit SIGHASH_ALL (accepted by BIP340 rules)
262        let with_hashtype = vec![
263            vec![0x01; 64]
264                .into_iter()
265                .chain([0x01u8])
266                .collect::<Vec<_>>(),
267        ];
268        assert!(validate_taproot_witness_structure(&with_hashtype, false).unwrap());
269
270        // Invalid: explicit SIGHASH_DEFAULT suffix
271        let invalid_hashtype = vec![
272            vec![0x01; 64]
273                .into_iter()
274                .chain([0x00u8])
275                .collect::<Vec<_>>(),
276        ];
277        assert!(!validate_taproot_witness_structure(&invalid_hashtype, false).unwrap());
278
279        // Invalid: multiple elements
280        let invalid2 = vec![vec![0x01; 64], vec![0x02; 32]];
281        assert!(!validate_taproot_witness_structure(&invalid2, false).unwrap());
282    }
283
284    #[test]
285    fn test_validate_taproot_witness_structure_script_path() {
286        // Script path: script + control block (33 bytes minimum)
287        let witness = vec![
288            vec![OP_1],    // Script
289            vec![0u8; 33], // Control block (internal key + leaf version + parity)
290        ];
291        assert!(validate_taproot_witness_structure(&witness, true).unwrap());
292
293        // Invalid: control block too small
294        let invalid = vec![vec![OP_1], vec![0u8; 32]];
295        assert!(!validate_taproot_witness_structure(&invalid, true).unwrap());
296
297        // Invalid: only one element
298        let invalid2 = vec![vec![OP_1]];
299        assert!(!validate_taproot_witness_structure(&invalid2, true).unwrap());
300    }
301
302    #[test]
303    fn test_calculate_transaction_weight_segwit() {
304        let base_size = 100;
305        let total_size = 150;
306        let weight = calculate_transaction_weight_segwit(base_size, total_size);
307        assert_eq!(weight, 3 * base_size + total_size); // BIP141
308    }
309
310    #[test]
311    fn test_weight_to_vsize() {
312        assert_eq!(weight_to_vsize(400), 100); // Exact division
313        assert_eq!(weight_to_vsize(401), 101); // Ceiling
314        assert_eq!(weight_to_vsize(403), 101); // Ceiling
315        assert_eq!(weight_to_vsize(404), 101); // Ceiling
316    }
317
318    #[test]
319    fn test_extract_witness_version() {
320        // P2WPKH: OP_0 PUSH_20 <20 bytes> — must be exactly 22 bytes (BIP141).
321        let mut segwit_script = vec![OP_0, PUSH_20_BYTES];
322        segwit_script.extend([0x01u8; 20]);
323        assert_eq!(
324            extract_witness_version(&segwit_script),
325            Some(WitnessVersion::SegWitV0)
326        );
327
328        // P2TR: OP_1 PUSH_32 <32 bytes> — must be exactly 34 bytes (BIP341).
329        let mut taproot_script = vec![OP_1, PUSH_32_BYTES];
330        taproot_script.extend([0x02u8; 32]);
331        assert_eq!(
332            extract_witness_version(&taproot_script),
333            Some(WitnessVersion::TaprootV1)
334        );
335
336        let non_witness_script = vec![OP_DUP, OP_HASH160]; // OP_DUP OP_HASH160
337        assert_eq!(extract_witness_version(&non_witness_script), None);
338    }
339
340    #[test]
341    fn test_extract_witness_program() {
342        // P2WPKH format: [OP_0, PUSH_20_BYTES, <20-byte-hash>]
343        // Where OP_0 is OP_0 (witness version), PUSH_20_BYTES is push 20 bytes, then 20 bytes of hash
344        // extract_witness_program should return just the program bytes (after push opcode)
345        // Note: 0x01 to PUSH_20_BYTES is 20 bytes (1, 2, 3, ..., 20)
346        let segwit_script = vec![
347            OP_0,
348            PUSH_20_BYTES,
349            0x01,
350            0x02,
351            0x03,
352            0x04,
353            0x05,
354            0x06,
355            0x07,
356            0x08,
357            0x09,
358            0x0a,
359            0x0b,
360            0x0c,
361            0x0d,
362            0x0e,
363            0x0f,
364            0x10,
365            0x11,
366            0x12,
367            0x13,
368            PUSH_20_BYTES,
369        ];
370        let program = extract_witness_program(&segwit_script, WitnessVersion::SegWitV0);
371        // Should return the 20 bytes after the push opcode (PUSH_20_BYTES)
372        assert_eq!(
373            program,
374            Some(vec![
375                0x01,
376                0x02,
377                0x03,
378                0x04,
379                0x05,
380                0x06,
381                0x07,
382                0x08,
383                0x09,
384                0x0a,
385                0x0b,
386                0x0c,
387                0x0d,
388                0x0e,
389                0x0f,
390                0x10,
391                0x11,
392                0x12,
393                0x13,
394                PUSH_20_BYTES
395            ])
396        );
397    }
398
399    #[test]
400    fn test_extract_witness_program_rejects_non_direct_push() {
401        use crate::opcodes::OP_PUSHDATA1;
402        // OP_PUSHDATA1 (0x4c) is not valid in a witness program scriptPubKey
403        let bad = vec![OP_0, OP_PUSHDATA1, 0x14u8];
404        assert_eq!(
405            extract_witness_program(&bad, WitnessVersion::SegWitV0),
406            None
407        );
408    }
409
410    #[test]
411    fn test_validate_witness_program_length() {
412        let p2wpkh = vec![0u8; 20]; // 20 bytes
413        assert!(validate_witness_program_length(
414            &p2wpkh,
415            WitnessVersion::SegWitV0
416        ));
417
418        let p2wsh = vec![0u8; 32]; // 32 bytes
419        assert!(validate_witness_program_length(
420            &p2wsh,
421            WitnessVersion::SegWitV0
422        ));
423
424        let p2tr = vec![0u8; 32]; // 32 bytes
425        assert!(validate_witness_program_length(
426            &p2tr,
427            WitnessVersion::TaprootV1
428        ));
429
430        let invalid = vec![0u8; 33];
431        assert!(!validate_witness_program_length(
432            &invalid,
433            WitnessVersion::SegWitV0
434        ));
435        assert!(!validate_witness_program_length(
436            &invalid,
437            WitnessVersion::TaprootV1
438        ));
439    }
440
441    #[test]
442    fn test_is_witness_empty() {
443        assert!(is_witness_empty(&vec![]));
444        assert!(is_witness_empty(&vec![vec![]]));
445        assert!(!is_witness_empty(&vec![vec![0x01]]));
446    }
447}