Skip to main content

tidecoin_consensus_core/
witness.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Shared Tidecoin witness-program parsing and validation helpers.
4
5use alloc::vec::Vec;
6
7use crate::{ScriptError, VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM, VERIFY_WITNESS_V1_512};
8use hashes::{sha256, sha512};
9use primitives::{
10    opcodes::all,
11    script::{
12        Builder as ScriptBuilderT, ParsedWitnessProgram, PushBytesBuf,
13        WitnessProgramClass as PrimitiveWitnessProgramClass,
14    },
15    Witness,
16};
17
18type Builder = ScriptBuilderT<()>;
19
20/// Consensus classification for a parsed witness program.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum WitnessProgramClass {
23    /// Native witness-v0 key-hash program.
24    P2wpkh,
25    /// Native witness-v0 script-hash program.
26    P2wsh,
27    /// Tidecoin witness-v1-512 program.
28    WitnessV1_512,
29    /// Future/upgradable witness version that is currently treated as no-op.
30    Upgradable,
31}
32
33/// Signature-hash mode used by witness execution.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WitnessSigVersion {
36    /// Segwit v0 signature hashing.
37    V0,
38    /// Tidecoin witness-v1-512 signature hashing.
39    V1_512,
40}
41
42/// Sigop accounting mode required before executing a witness script.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum WitnessSigops {
45    /// No extra sigops need to be accounted for before execution.
46    None,
47    /// A fixed number of sigops should be added.
48    Fixed(u32),
49    /// Sigops should be counted from the executed script.
50    CountExecutedScript,
51}
52
53/// Shared execution plan for a validated witness program.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum WitnessExecutionPlan {
56    /// Upgradable witness program versions are accepted as no-op under current flags.
57    Upgradable,
58    /// Execute the provided script with the prepared witness stack.
59    Execute {
60        /// Signature-hash mode to use while executing the script.
61        sigversion: WitnessSigVersion,
62        /// Script bytes to execute.
63        script_bytes: Vec<u8>,
64        /// Initial witness stack items, excluding the executed script when present.
65        stack_items: Vec<Vec<u8>>,
66        /// Additional sigop accounting required before execution.
67        sigops: WitnessSigops,
68    },
69}
70
71/// Parsed witness program view over a scriptPubKey byte slice.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct WitnessProgram<'a>(ParsedWitnessProgram<'a>);
74
75impl<'a> WitnessProgram<'a> {
76    /// Creates a witness program view from an already-parsed version and program.
77    pub fn parse_program(version: u8, program: &'a [u8]) -> Self {
78        Self(ParsedWitnessProgram::from_program(version, program))
79    }
80
81    /// Parses a witness program from scriptPubKey bytes.
82    pub fn parse(script_bytes: &'a [u8]) -> Option<Self> {
83        ParsedWitnessProgram::parse_script_pubkey(script_bytes).map(Self)
84    }
85
86    /// Returns the witness version.
87    pub fn version(self) -> u8 {
88        self.0.version()
89    }
90
91    /// Returns the witness program bytes.
92    pub fn program(self) -> &'a [u8] {
93        self.0.program()
94    }
95
96    /// Returns whether the script is a Tidecoin witness-v1-512 program.
97    pub fn is_v1_512(script_bytes: &'a [u8]) -> bool {
98        Self::parse(script_bytes)
99            .is_some_and(|program| program.0.class() == PrimitiveWitnessProgramClass::P2wsh512)
100    }
101
102    /// Classifies the parsed witness program under the provided verification flags.
103    pub fn classify(self, flags: u32) -> Result<WitnessProgramClass, ScriptError> {
104        match self.version() {
105            0 => match self.0.class() {
106                PrimitiveWitnessProgramClass::P2wpkh => Ok(WitnessProgramClass::P2wpkh),
107                PrimitiveWitnessProgramClass::P2wsh => Ok(WitnessProgramClass::P2wsh),
108                PrimitiveWitnessProgramClass::P2wsh512
109                | PrimitiveWitnessProgramClass::P2a
110                | PrimitiveWitnessProgramClass::Upgradable => {
111                    Err(ScriptError::WitnessProgramWrongLength)
112                }
113            },
114            1 => {
115                if flags & VERIFY_WITNESS_V1_512 == 0 {
116                    if flags & VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM != 0 {
117                        Err(ScriptError::DiscourageUpgradableWitnessProgram)
118                    } else {
119                        Ok(WitnessProgramClass::Upgradable)
120                    }
121                } else {
122                    match self.0.class() {
123                        PrimitiveWitnessProgramClass::P2wsh512 => {
124                            Ok(WitnessProgramClass::WitnessV1_512)
125                        }
126                        PrimitiveWitnessProgramClass::P2wpkh
127                        | PrimitiveWitnessProgramClass::P2wsh
128                        | PrimitiveWitnessProgramClass::P2a
129                        | PrimitiveWitnessProgramClass::Upgradable => {
130                            Err(ScriptError::WitnessProgramWrongLength)
131                        }
132                    }
133                }
134            }
135            2..=16 => {
136                if flags & VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM != 0 {
137                    Err(ScriptError::DiscourageUpgradableWitnessProgram)
138                } else {
139                    Ok(WitnessProgramClass::Upgradable)
140                }
141            }
142            _ => Ok(WitnessProgramClass::Upgradable),
143        }
144    }
145
146    /// Builds an execution plan for this witness program and witness stack.
147    pub fn execution_plan(
148        self,
149        flags: u32,
150        witness: &Witness,
151    ) -> Result<WitnessExecutionPlan, ScriptError> {
152        match self.classify(flags)? {
153            WitnessProgramClass::P2wpkh => build_p2wpkh_plan(self.program(), witness),
154            WitnessProgramClass::P2wsh => build_p2wsh_plan(self.program(), witness),
155            WitnessProgramClass::WitnessV1_512 => {
156                build_witness_v1_512_plan(self.program(), witness)
157            }
158            WitnessProgramClass::Upgradable => Ok(WitnessExecutionPlan::Upgradable),
159        }
160    }
161}
162
163fn build_p2wpkh_plan(
164    program: &[u8],
165    witness: &Witness,
166) -> Result<WitnessExecutionPlan, ScriptError> {
167    if witness.len() != 2 {
168        return Err(ScriptError::WitnessProgramMismatch);
169    }
170
171    let program_bytes = PushBytesBuf::try_from(program.to_vec())
172        .map_err(|_| ScriptError::WitnessProgramWrongLength)?;
173    let script = Builder::new()
174        .push_opcode(all::OP_DUP)
175        .push_opcode(all::OP_HASH160)
176        .push_slice(program_bytes)
177        .push_opcode(all::OP_EQUALVERIFY)
178        .push_opcode(all::OP_CHECKSIG)
179        .into_script();
180
181    Ok(WitnessExecutionPlan::Execute {
182        sigversion: WitnessSigVersion::V0,
183        script_bytes: script.into_bytes(),
184        stack_items: witness_items(witness, witness.len()),
185        sigops: WitnessSigops::Fixed(1),
186    })
187}
188
189fn build_p2wsh_plan(
190    program: &[u8],
191    witness: &Witness,
192) -> Result<WitnessExecutionPlan, ScriptError> {
193    if witness.is_empty() {
194        return Err(ScriptError::WitnessProgramWitnessEmpty);
195    }
196
197    let witness_script_bytes = witness[witness.len() - 1].as_ref();
198    let script_hash = sha256::Hash::hash(witness_script_bytes);
199    let hash_bytes: &[u8] = script_hash.as_ref();
200    if hash_bytes != program {
201        return Err(ScriptError::WitnessProgramMismatch);
202    }
203
204    Ok(WitnessExecutionPlan::Execute {
205        sigversion: WitnessSigVersion::V0,
206        script_bytes: witness_script_bytes.to_vec(),
207        stack_items: witness_items(witness, witness.len() - 1),
208        sigops: WitnessSigops::CountExecutedScript,
209    })
210}
211
212fn build_witness_v1_512_plan(
213    program: &[u8],
214    witness: &Witness,
215) -> Result<WitnessExecutionPlan, ScriptError> {
216    if witness.is_empty() {
217        return Err(ScriptError::WitnessProgramWitnessEmpty);
218    }
219
220    let exec_script_bytes = witness[witness.len() - 1].as_ref();
221    let script_hash = sha512::Hash::hash(exec_script_bytes);
222    if script_hash.as_byte_array() != program {
223        return Err(ScriptError::WitnessProgramMismatch);
224    }
225
226    Ok(WitnessExecutionPlan::Execute {
227        sigversion: WitnessSigVersion::V1_512,
228        script_bytes: exec_script_bytes.to_vec(),
229        stack_items: witness_items(witness, witness.len() - 1),
230        sigops: WitnessSigops::CountExecutedScript,
231    })
232}
233
234fn witness_items(witness: &Witness, end: usize) -> Vec<Vec<u8>> {
235    witness.iter().take(end).map(|elem| elem.to_vec()).collect()
236}
237
238#[cfg(test)]
239mod tests {
240    use alloc::vec;
241
242    use super::{
243        WitnessExecutionPlan, WitnessProgram, WitnessProgramClass, WitnessSigVersion, WitnessSigops,
244    };
245    use crate::{ScriptError, VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM, VERIFY_WITNESS_V1_512};
246    use hashes::sha512;
247    use primitives::Witness;
248
249    #[test]
250    fn parses_witness_v0_program() {
251        let script = [
252            0x00, 0x20, 1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
253            0, 0, 0, 0, 0, 0, 0, 0,
254        ];
255        let program = WitnessProgram::parse(&script).expect("witness program");
256        assert_eq!(program.version(), 0);
257        assert_eq!(program.program().len(), 32);
258    }
259
260    #[test]
261    fn detects_tidecoin_witness_v1_512_program() {
262        let mut script = vec![0x51, 64];
263        script.extend_from_slice(&[7u8; 64]);
264        let program = WitnessProgram::parse(&script).expect("witness program");
265        assert_eq!(program.version(), 1);
266        assert_eq!(program.program(), &[7u8; 64]);
267        assert!(WitnessProgram::is_v1_512(&script));
268    }
269
270    #[test]
271    fn rejects_noncanonical_push_length() {
272        let script = [0x51, 0x4c, 0x40];
273        assert!(WitnessProgram::parse(&script).is_none());
274    }
275
276    #[test]
277    fn classifies_witness_v0_program_lengths() {
278        let p2wpkh = WitnessProgram::parse(&[
279            0x00, 20, 9u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
280        ])
281        .expect("p2wpkh");
282        assert_eq!(p2wpkh.classify(0).expect("classify"), WitnessProgramClass::P2wpkh);
283
284        let mut p2wsh_bytes = vec![0x00, 32];
285        p2wsh_bytes.extend_from_slice(&[5u8; 32]);
286        let p2wsh = WitnessProgram::parse(&p2wsh_bytes).expect("p2wsh");
287        assert_eq!(p2wsh.classify(0).expect("classify"), WitnessProgramClass::P2wsh);
288    }
289
290    #[test]
291    fn witness_v1_512_requires_feature_flag() {
292        let mut script = vec![0x51, 64];
293        script.extend_from_slice(&[1u8; 64]);
294        let witness_program = WitnessProgram::parse(&script).expect("witness v1");
295
296        assert_eq!(
297            witness_program.classify(0).expect("upgradable"),
298            WitnessProgramClass::Upgradable
299        );
300        assert_eq!(
301            witness_program.classify(VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM),
302            Err(ScriptError::DiscourageUpgradableWitnessProgram)
303        );
304        assert_eq!(
305            witness_program.classify(VERIFY_WITNESS_V1_512).expect("v1 enabled"),
306            WitnessProgramClass::WitnessV1_512
307        );
308    }
309
310    #[test]
311    fn builds_p2wpkh_execution_plan() {
312        let mut script = vec![0x00, 20];
313        script.extend_from_slice(&[3u8; 20]);
314        let witness_program = WitnessProgram::parse(&script).expect("p2wpkh");
315        let witness = Witness::from(vec![vec![1u8; 64], vec![2u8; 33]]);
316        let plan = witness_program.execution_plan(0, &witness).expect("plan");
317
318        match plan {
319            WitnessExecutionPlan::Execute { sigversion, script_bytes, stack_items, sigops } => {
320                assert_eq!(sigversion, WitnessSigVersion::V0);
321                assert_eq!(sigops, WitnessSigops::Fixed(1));
322                assert_eq!(stack_items.len(), 2);
323                assert!(!script_bytes.is_empty());
324            }
325            WitnessExecutionPlan::Upgradable => panic!("unexpected upgradable plan"),
326        }
327    }
328
329    #[test]
330    fn builds_witness_v1_512_execution_plan() {
331        let exec_script = vec![0x51];
332        let program_hash = sha512::Hash::hash(&exec_script);
333        let mut script = vec![0x51, 64];
334        script.extend_from_slice(program_hash.as_byte_array());
335        let witness_program = WitnessProgram::parse(&script).expect("witness v1");
336        let witness = Witness::from(vec![vec![1u8], exec_script.clone()]);
337        let plan = witness_program.execution_plan(VERIFY_WITNESS_V1_512, &witness).expect("plan");
338
339        match plan {
340            WitnessExecutionPlan::Execute { sigversion, script_bytes, stack_items, sigops } => {
341                assert_eq!(sigversion, WitnessSigVersion::V1_512);
342                assert_eq!(sigops, WitnessSigops::CountExecutedScript);
343                assert_eq!(script_bytes, exec_script);
344                assert_eq!(stack_items, vec![vec![1u8]]);
345            }
346            WitnessExecutionPlan::Upgradable => panic!("unexpected upgradable plan"),
347        }
348    }
349}