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