Skip to main content

tidecoin_primitives/script/
witness_program.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Primitive witness-program parsing and classification.
4
5use core::convert::Infallible;
6use core::fmt;
7
8/// Minimum byte size of a witness program.
9pub const WITNESS_PROGRAM_MIN_SIZE: usize = 2;
10/// Maximum byte size of a witness program.
11pub const WITNESS_PROGRAM_MAX_SIZE: usize = 64;
12/// The Tidecoin pay-to-anchor witness-v1 program.
13pub const P2A_PROGRAM: [u8; 2] = [78, 115];
14
15/// Primitive witness-program class independent of consensus activation flags.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum WitnessProgramClass {
18    /// Native witness-v0 key-hash program.
19    P2wpkh,
20    /// Native witness-v0 script-hash program.
21    P2wsh,
22    /// Tidecoin witness-v1 SHA-512 script-hash program.
23    P2wsh512,
24    /// Tidecoin pay-to-anchor program.
25    P2a,
26    /// Any currently unassigned witness program.
27    Upgradable,
28}
29
30/// Witness-program shape error.
31#[derive(Clone, Debug, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum WitnessProgramError {
34    /// The witness program must be between 2 and 64 bytes in length.
35    InvalidLength(usize),
36    /// A v0 witness program must be either 20 or 32 bytes.
37    InvalidSegwitV0Length(usize),
38}
39
40impl From<Infallible> for WitnessProgramError {
41    fn from(never: Infallible) -> Self {
42        match never {}
43    }
44}
45
46impl fmt::Display for WitnessProgramError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::InvalidLength(len) => {
50                write!(f, "witness program must be between 2 and 64 bytes: length={}", len)
51            }
52            Self::InvalidSegwitV0Length(len) => {
53                write!(f, "a v0 witness program must be either 20 or 32 bytes: length={}", len)
54            }
55        }
56    }
57}
58
59#[cfg(feature = "std")]
60impl std::error::Error for WitnessProgramError {
61    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
62        match self {
63            Self::InvalidLength(_) | Self::InvalidSegwitV0Length(_) => None,
64        }
65    }
66}
67
68/// Parsed witness-program view over a scriptPubKey byte slice.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct ParsedWitnessProgram<'a> {
71    version: u8,
72    program: &'a [u8],
73}
74
75impl<'a> ParsedWitnessProgram<'a> {
76    /// Creates a witness-program view from an already parsed version and program.
77    pub fn from_program(version: u8, program: &'a [u8]) -> Self {
78        Self { version, program }
79    }
80
81    /// Parses a canonical direct-push witness program from scriptPubKey bytes.
82    pub fn parse_script_pubkey(script_bytes: &'a [u8]) -> Option<Self> {
83        if script_bytes.len() < WITNESS_PROGRAM_MIN_SIZE + 2 {
84            return None;
85        }
86
87        let version = match script_bytes[0] {
88            0x00 => 0,
89            0x51..=0x60 => script_bytes[0] - 0x50,
90            _ => return None,
91        };
92        let push_len = parse_direct_push_len(script_bytes.get(1).copied())?;
93        if script_bytes.len() != push_len + 2 {
94            return None;
95        }
96
97        Some(Self { version, program: &script_bytes[2..] })
98    }
99
100    /// Returns the witness version number.
101    pub fn version(self) -> u8 {
102        self.version
103    }
104
105    /// Returns the witness program bytes.
106    pub fn program(self) -> &'a [u8] {
107        self.program
108    }
109
110    /// Classifies this witness program without applying consensus activation flags.
111    pub fn class(self) -> WitnessProgramClass {
112        classify_witness_program(self.version, self.program)
113    }
114}
115
116/// Validates address/output-level witness-program length rules.
117///
118/// # Errors
119///
120/// Returns [`WitnessProgramError::InvalidLength`] when the program is outside the canonical
121/// 2-byte to 64-byte witness-program range, and [`WitnessProgramError::InvalidSegwitV0Length`]
122/// when a witness-v0 program is not 20 or 32 bytes.
123pub fn validate_witness_program(
124    version: u8,
125    program_len: usize,
126) -> Result<(), WitnessProgramError> {
127    if !(WITNESS_PROGRAM_MIN_SIZE..=WITNESS_PROGRAM_MAX_SIZE).contains(&program_len) {
128        return Err(WitnessProgramError::InvalidLength(program_len));
129    }
130    if version == 0 && program_len != 20 && program_len != 32 {
131        return Err(WitnessProgramError::InvalidSegwitV0Length(program_len));
132    }
133    Ok(())
134}
135
136/// Classifies a witness program without applying consensus activation flags.
137pub fn classify_witness_program(version: u8, program: &[u8]) -> WitnessProgramClass {
138    match (version, program.len()) {
139        (0, 20) => WitnessProgramClass::P2wpkh,
140        (0, 32) => WitnessProgramClass::P2wsh,
141        (1, 64) => WitnessProgramClass::P2wsh512,
142        (1, 2) if program == P2A_PROGRAM => WitnessProgramClass::P2a,
143        _ => WitnessProgramClass::Upgradable,
144    }
145}
146
147fn parse_direct_push_len(opcode: Option<u8>) -> Option<usize> {
148    let value = opcode?;
149    if value <= 75 {
150        Some(value as usize)
151    } else {
152        None
153    }
154}