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![vec![0x01; 64]
266            .into_iter()
267            .chain([0x01u8])
268            .collect::<Vec<_>>()];
269        assert!(validate_taproot_witness_structure(&with_hashtype, false).unwrap());
270
271        // Invalid: explicit SIGHASH_DEFAULT suffix
272        let invalid_hashtype = vec![vec![0x01; 64]
273            .into_iter()
274            .chain([0x00u8])
275            .collect::<Vec<_>>()];
276        assert!(!validate_taproot_witness_structure(&invalid_hashtype, false).unwrap());
277
278        // Invalid: multiple elements
279        let invalid2 = vec![vec![0x01; 64], vec![0x02; 32]];
280        assert!(!validate_taproot_witness_structure(&invalid2, false).unwrap());
281    }
282
283    #[test]
284    fn test_validate_taproot_witness_structure_script_path() {
285        // Script path: script + control block (33 bytes minimum)
286        let witness = vec![
287            vec![OP_1],    // Script
288            vec![0u8; 33], // Control block (internal key + leaf version + parity)
289        ];
290        assert!(validate_taproot_witness_structure(&witness, true).unwrap());
291
292        // Invalid: control block too small
293        let invalid = vec![vec![OP_1], vec![0u8; 32]];
294        assert!(!validate_taproot_witness_structure(&invalid, true).unwrap());
295
296        // Invalid: only one element
297        let invalid2 = vec![vec![OP_1]];
298        assert!(!validate_taproot_witness_structure(&invalid2, true).unwrap());
299    }
300
301    #[test]
302    fn test_calculate_transaction_weight_segwit() {
303        let base_size = 100;
304        let total_size = 150;
305        let weight = calculate_transaction_weight_segwit(base_size, total_size);
306        assert_eq!(weight, 3 * base_size + total_size); // BIP141
307    }
308
309    #[test]
310    fn test_weight_to_vsize() {
311        assert_eq!(weight_to_vsize(400), 100); // Exact division
312        assert_eq!(weight_to_vsize(401), 101); // Ceiling
313        assert_eq!(weight_to_vsize(403), 101); // Ceiling
314        assert_eq!(weight_to_vsize(404), 101); // Ceiling
315    }
316
317    #[test]
318    fn test_extract_witness_version() {
319        // P2WPKH: OP_0 PUSH_20 <20 bytes> — must be exactly 22 bytes (BIP141).
320        let mut segwit_script = vec![OP_0, PUSH_20_BYTES];
321        segwit_script.extend([0x01u8; 20]);
322        assert_eq!(
323            extract_witness_version(&segwit_script),
324            Some(WitnessVersion::SegWitV0)
325        );
326
327        // P2TR: OP_1 PUSH_32 <32 bytes> — must be exactly 34 bytes (BIP341).
328        let mut taproot_script = vec![OP_1, PUSH_32_BYTES];
329        taproot_script.extend([0x02u8; 32]);
330        assert_eq!(
331            extract_witness_version(&taproot_script),
332            Some(WitnessVersion::TaprootV1)
333        );
334
335        let non_witness_script = vec![OP_DUP, OP_HASH160]; // OP_DUP OP_HASH160
336        assert_eq!(extract_witness_version(&non_witness_script), None);
337    }
338
339    #[test]
340    fn test_extract_witness_program() {
341        // P2WPKH format: [OP_0, PUSH_20_BYTES, <20-byte-hash>]
342        // Where OP_0 is OP_0 (witness version), PUSH_20_BYTES is push 20 bytes, then 20 bytes of hash
343        // extract_witness_program should return just the program bytes (after push opcode)
344        // Note: 0x01 to PUSH_20_BYTES is 20 bytes (1, 2, 3, ..., 20)
345        let segwit_script = vec![
346            OP_0,
347            PUSH_20_BYTES,
348            0x01,
349            0x02,
350            0x03,
351            0x04,
352            0x05,
353            0x06,
354            0x07,
355            0x08,
356            0x09,
357            0x0a,
358            0x0b,
359            0x0c,
360            0x0d,
361            0x0e,
362            0x0f,
363            0x10,
364            0x11,
365            0x12,
366            0x13,
367            PUSH_20_BYTES,
368        ];
369        let program = extract_witness_program(&segwit_script, WitnessVersion::SegWitV0);
370        // Should return the 20 bytes after the push opcode (PUSH_20_BYTES)
371        assert_eq!(
372            program,
373            Some(vec![
374                0x01,
375                0x02,
376                0x03,
377                0x04,
378                0x05,
379                0x06,
380                0x07,
381                0x08,
382                0x09,
383                0x0a,
384                0x0b,
385                0x0c,
386                0x0d,
387                0x0e,
388                0x0f,
389                0x10,
390                0x11,
391                0x12,
392                0x13,
393                PUSH_20_BYTES
394            ])
395        );
396    }
397
398    #[test]
399    fn test_validate_witness_program_length() {
400        let p2wpkh = vec![0u8; 20]; // 20 bytes
401        assert!(validate_witness_program_length(
402            &p2wpkh,
403            WitnessVersion::SegWitV0
404        ));
405
406        let p2wsh = vec![0u8; 32]; // 32 bytes
407        assert!(validate_witness_program_length(
408            &p2wsh,
409            WitnessVersion::SegWitV0
410        ));
411
412        let p2tr = vec![0u8; 32]; // 32 bytes
413        assert!(validate_witness_program_length(
414            &p2tr,
415            WitnessVersion::TaprootV1
416        ));
417
418        let invalid = vec![0u8; 33];
419        assert!(!validate_witness_program_length(
420            &invalid,
421            WitnessVersion::SegWitV0
422        ));
423        assert!(!validate_witness_program_length(
424            &invalid,
425            WitnessVersion::TaprootV1
426        ));
427    }
428
429    #[test]
430    fn test_is_witness_empty() {
431        assert!(is_witness_empty(&vec![]));
432        assert!(is_witness_empty(&vec![vec![]]));
433        assert!(!is_witness_empty(&vec![vec![0x01]]));
434    }
435}