Skip to main content

chains_sdk/bitcoin/
tapscript.rs

1//! **BIP-342** — Tapscript: validation rules for Taproot script-path spends.
2//!
3//! Provides a script builder and validator for Tapscript, which uses BIP-340
4//! Schnorr signatures instead of legacy ECDSA.
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::bitcoin::tapscript::{Script, Opcode};
9//!
10//! // Simple "check signature" script
11//! let pubkey = [0xAA_u8; 32];
12//! let script = Script::new()
13//!     .push_key(&pubkey)
14//!     .push_opcode(Opcode::OP_CHECKSIG);
15//! ```
16
17use sha2::{Digest, Sha256};
18
19// ─── Opcodes ────────────────────────────────────────────────────────
20
21/// Bitcoin/Tapscript opcodes.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23#[repr(u8)]
24#[allow(non_camel_case_types)]
25pub enum Opcode {
26    /// Push empty byte vector.
27    OP_0 = 0x00,
28    /// Push the number 1 (OP_TRUE).
29    OP_1 = 0x51,
30    /// Push the number 2.
31    OP_2 = 0x52,
32    /// Push the number 3.
33    OP_3 = 0x53,
34    /// Push the number 16.
35    OP_16 = 0x60,
36    /// Return immediately (marks output as unspendable/data carrier).
37    OP_RETURN = 0x6a,
38    /// Duplicate the top stack item.
39    OP_DUP = 0x76,
40    /// Pop and check equality.
41    OP_EQUAL = 0x87,
42    /// OP_EQUAL + OP_VERIFY.
43    OP_EQUALVERIFY = 0x88,
44    /// Verify top stack item is nonzero, fail if not.
45    OP_VERIFY = 0x69,
46    /// Pop two items, fail if not equal.
47    OP_HASH160 = 0xa9,
48    /// BIP-340 Schnorr signature check (replaces legacy OP_CHECKSIG).
49    OP_CHECKSIG = 0xac,
50    /// BIP-342: Schnorr sig check + accumulate counter.
51    OP_CHECKSIGADD = 0xba,
52    /// Check that the top stack item equals the required number of signatures.
53    OP_NUMEQUAL = 0x9c,
54    /// OP_NUMEQUAL + OP_VERIFY.
55    OP_NUMEQUALVERIFY = 0x9d,
56    /// Check locktime.
57    OP_CHECKLOCKTIMEVERIFY = 0xb1,
58    /// Check sequence.
59    OP_CHECKSEQUENCEVERIFY = 0xb2,
60    /// Mark remaining script as always-succeeding (BIP-342).
61    OP_SUCCESS = 0x50,
62    /// No operation.
63    OP_NOP = 0x61,
64    /// Drop top stack item.
65    OP_DROP = 0x75,
66    /// Swap top two stack items.
67    OP_SWAP = 0x7c,
68    /// If top stack item is true, execute following script.
69    OP_IF = 0x63,
70    /// Else branch for OP_IF.
71    OP_ELSE = 0x67,
72    /// End if block.
73    OP_ENDIF = 0x68,
74}
75
76impl From<Opcode> for u8 {
77    fn from(op: Opcode) -> u8 {
78        op as u8
79    }
80}
81
82// ─── Script Builder ─────────────────────────────────────────────────
83
84/// A Tapscript builder for constructing Bitcoin scripts.
85#[derive(Clone, Debug)]
86pub struct Script {
87    /// The raw script bytes.
88    bytes: Vec<u8>,
89}
90
91impl Script {
92    /// Create a new empty script.
93    pub fn new() -> Self {
94        Self { bytes: Vec::new() }
95    }
96
97    /// Push an opcode onto the script.
98    pub fn push_opcode(mut self, opcode: Opcode) -> Self {
99        self.bytes.push(opcode as u8);
100        self
101    }
102
103    /// Push raw data onto the script with appropriate push opcode.
104    ///
105    /// Automatically selects the correct push operation based on data length:
106    /// - 1-75 bytes: `OP_PUSHBYTESn`
107    /// - 76-255 bytes: `OP_PUSHDATA1`
108    /// - 256-65535 bytes: `OP_PUSHDATA2`
109    pub fn push_data(mut self, data: &[u8]) -> Self {
110        let len = data.len();
111        if len == 0 {
112            self.bytes.push(0x00); // OP_0
113        } else if len <= 75 {
114            self.bytes.push(len as u8); // OP_PUSHBYTESn
115            self.bytes.extend_from_slice(data);
116        } else if len <= 255 {
117            self.bytes.push(0x4c); // OP_PUSHDATA1
118            self.bytes.push(len as u8);
119            self.bytes.extend_from_slice(data);
120        } else if len <= 65535 {
121            self.bytes.push(0x4d); // OP_PUSHDATA2
122            self.bytes.extend_from_slice(&(len as u16).to_le_bytes());
123            self.bytes.extend_from_slice(data);
124        }
125        self
126    }
127
128    /// Push a 32-byte x-only public key (BIP-340 Schnorr key).
129    pub fn push_key(self, x_only_pubkey: &[u8; 32]) -> Self {
130        self.push_data(x_only_pubkey)
131    }
132
133    /// Push a raw byte.
134    pub fn push_byte(mut self, byte: u8) -> Self {
135        self.bytes.push(byte);
136        self
137    }
138
139    /// Push an integer (as minimal script encoding).
140    pub fn push_int(mut self, value: i64) -> Self {
141        if value == 0 {
142            self.bytes.push(0x00); // OP_0
143        } else if value == -1 {
144            self.bytes.push(0x4f); // OP_1NEGATE
145        } else if (1..=16).contains(&value) {
146            self.bytes.push(0x50 + value as u8); // OP_1 through OP_16
147        } else {
148            // Encode as minimal-length byte sequence
149            let mut v = value.unsigned_abs();
150            let negative = value < 0;
151            let mut encoded = Vec::new();
152            while v > 0 {
153                encoded.push((v & 0xFF) as u8);
154                v >>= 8;
155            }
156            // Add sign bit
157            if encoded.last().is_some_and(|b| b & 0x80 != 0) {
158                encoded.push(if negative { 0x80 } else { 0x00 });
159            } else if negative {
160                if let Some(last) = encoded.last_mut() {
161                    *last |= 0x80;
162                }
163            }
164            self = self.push_data(&encoded);
165        }
166        self
167    }
168
169    /// Get the raw script bytes.
170    pub fn to_bytes(&self) -> &[u8] {
171        &self.bytes
172    }
173
174    /// Consume and return the raw script bytes.
175    pub fn into_bytes(self) -> Vec<u8> {
176        self.bytes
177    }
178
179    /// Get the script length in bytes.
180    pub fn len(&self) -> usize {
181        self.bytes.len()
182    }
183
184    /// Check if the script is empty.
185    pub fn is_empty(&self) -> bool {
186        self.bytes.is_empty()
187    }
188
189    /// Compute the script hash (SHA256).
190    pub fn script_hash(&self) -> [u8; 32] {
191        let result = Sha256::digest(&self.bytes);
192        let mut hash = [0u8; 32];
193        hash.copy_from_slice(&result);
194        hash
195    }
196}
197
198impl Default for Script {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204// ─── Common Script Templates ────────────────────────────────────────
205
206/// Create a simple 1-of-1 Tapscript key-spend script.
207///
208/// `<pubkey> OP_CHECKSIG`
209pub fn checksig_script(x_only_pubkey: &[u8; 32]) -> Script {
210    Script::new()
211        .push_key(x_only_pubkey)
212        .push_opcode(Opcode::OP_CHECKSIG)
213}
214
215/// Create a Tapscript multisig script using OP_CHECKSIGADD.
216///
217/// BIP-342 replaces OP_CHECKMULTISIG with OP_CHECKSIGADD:
218/// ```text
219/// <key1> OP_CHECKSIG <key2> OP_CHECKSIGADD ... <keyN> OP_CHECKSIGADD <M> OP_NUMEQUALVERIFY
220/// ```
221pub fn multisig_script(keys: &[[u8; 32]], threshold: u32) -> Script {
222    let mut script = Script::new();
223
224    for (i, key) in keys.iter().enumerate() {
225        script = script.push_key(key);
226        if i == 0 {
227            script = script.push_opcode(Opcode::OP_CHECKSIG);
228        } else {
229            script = script.push_opcode(Opcode::OP_CHECKSIGADD);
230        }
231    }
232
233    script = script.push_int(threshold as i64);
234    script.push_opcode(Opcode::OP_NUMEQUALVERIFY)
235}
236
237/// Create a timelocked script.
238///
239/// `<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <pubkey> OP_CHECKSIG`
240pub fn timelocked_script(x_only_pubkey: &[u8; 32], locktime: u32) -> Script {
241    Script::new()
242        .push_int(locktime as i64)
243        .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY)
244        .push_opcode(Opcode::OP_DROP)
245        .push_key(x_only_pubkey)
246        .push_opcode(Opcode::OP_CHECKSIG)
247}
248
249// ─── Annex ──────────────────────────────────────────────────────────
250
251/// Check if a witness item is an annex (BIP-341).
252///
253/// An annex is identified by a `0x50` prefix byte.
254pub fn is_annex(data: &[u8]) -> bool {
255    data.first() == Some(&0x50)
256}
257
258/// Create an annex field from data.
259///
260/// Prefixes the data with `0x50`.
261pub fn create_annex(data: &[u8]) -> Vec<u8> {
262    let mut annex = Vec::with_capacity(1 + data.len());
263    annex.push(0x50);
264    annex.extend_from_slice(data);
265    annex
266}
267
268// ─── Signature Hash (Sighash) ──────────────────────────────────────
269
270/// Tapscript signature hash types.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272#[repr(u8)]
273pub enum SighashType {
274    /// Default (same as ALL for Taproot).
275    Default = 0x00,
276    /// Sign all inputs and outputs.
277    All = 0x01,
278    /// Sign all inputs, no outputs.
279    None = 0x02,
280    /// Sign all inputs, only the output at same index.
281    Single = 0x03,
282    /// AnyoneCanPay modifier (can be combined with above).
283    AllAnyoneCanPay = 0x81,
284    /// AnyoneCanPay + None.
285    NoneAnyoneCanPay = 0x82,
286    /// AnyoneCanPay + Single.
287    SingleAnyoneCanPay = 0x83,
288}
289
290impl SighashType {
291    /// Parse a sighash type byte.
292    pub fn from_byte(byte: u8) -> Option<Self> {
293        match byte {
294            0x00 => Some(SighashType::Default),
295            0x01 => Some(SighashType::All),
296            0x02 => Some(SighashType::None),
297            0x03 => Some(SighashType::Single),
298            0x81 => Some(SighashType::AllAnyoneCanPay),
299            0x82 => Some(SighashType::NoneAnyoneCanPay),
300            0x83 => Some(SighashType::SingleAnyoneCanPay),
301            _ => None,
302        }
303    }
304
305    /// Convert to the byte representation.
306    #[must_use]
307    pub fn to_byte(self) -> u8 {
308        self as u8
309    }
310}
311
312// ─── Tests ──────────────────────────────────────────────────────────
313
314#[cfg(test)]
315#[allow(clippy::unwrap_used, clippy::expect_used)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_script_builder_empty() {
321        let script = Script::new();
322        assert!(script.is_empty());
323        assert_eq!(script.len(), 0);
324    }
325
326    #[test]
327    fn test_script_builder_opcode() {
328        let script = Script::new().push_opcode(Opcode::OP_CHECKSIG);
329        assert_eq!(script.to_bytes(), &[0xac]);
330    }
331
332    #[test]
333    fn test_script_builder_key_checksig() {
334        let key = [0xAA; 32];
335        let script = checksig_script(&key);
336        let bytes = script.to_bytes();
337        assert_eq!(bytes[0], 32); // push 32 bytes
338        assert_eq!(&bytes[1..33], &key);
339        assert_eq!(bytes[33], 0xac); // OP_CHECKSIG
340        assert_eq!(bytes.len(), 34);
341    }
342
343    #[test]
344    fn test_script_builder_push_data_small() {
345        let data = vec![0x01, 0x02, 0x03];
346        let script = Script::new().push_data(&data);
347        assert_eq!(script.to_bytes()[0], 3); // length prefix
348        assert_eq!(&script.to_bytes()[1..], &data);
349    }
350
351    #[test]
352    fn test_script_builder_push_data_76() {
353        let data = vec![0xFF; 76];
354        let script = Script::new().push_data(&data);
355        assert_eq!(script.to_bytes()[0], 0x4c); // OP_PUSHDATA1
356        assert_eq!(script.to_bytes()[1], 76); // length
357        assert_eq!(&script.to_bytes()[2..], &data[..]);
358    }
359
360    #[test]
361    fn test_script_builder_push_int_small() {
362        let s0 = Script::new().push_int(0);
363        assert_eq!(s0.to_bytes(), &[0x00]); // OP_0
364
365        let s1 = Script::new().push_int(1);
366        assert_eq!(s1.to_bytes(), &[0x51]); // OP_1
367
368        let s16 = Script::new().push_int(16);
369        assert_eq!(s16.to_bytes(), &[0x60]); // OP_16
370    }
371
372    #[test]
373    fn test_script_builder_push_int_negative() {
374        let s = Script::new().push_int(-1);
375        assert_eq!(s.to_bytes(), &[0x4f]); // OP_1NEGATE
376    }
377
378    #[test]
379    fn test_multisig_script_2_of_3() {
380        let k1 = [0x01; 32];
381        let k2 = [0x02; 32];
382        let k3 = [0x03; 32];
383        let script = multisig_script(&[k1, k2, k3], 2);
384        let bytes = script.to_bytes();
385
386        // Should contain all 3 keys
387        assert!(!bytes.is_empty());
388        // First key push: 32 bytes + OP_CHECKSIG
389        assert_eq!(bytes[0], 32); // push 32
390                                  // Should end with 2 OP_NUMEQUALVERIFY (push 2 + 0x9d)
391        let last = bytes[bytes.len() - 1];
392        assert_eq!(last, 0x9d); // OP_NUMEQUALVERIFY
393    }
394
395    #[test]
396    fn test_timelocked_script() {
397        let key = [0xAA; 32];
398        let script = timelocked_script(&key, 500000);
399        let bytes = script.to_bytes();
400        assert!(!bytes.is_empty());
401        // Should contain OP_CHECKLOCKTIMEVERIFY
402        assert!(bytes.contains(&0xb1));
403        // Should contain OP_DROP
404        assert!(bytes.contains(&0x75));
405        // Should contain OP_CHECKSIG
406        assert!(bytes.contains(&0xac));
407    }
408
409    #[test]
410    fn test_annex_identification() {
411        assert!(is_annex(&[0x50, 0x01, 0x02]));
412        assert!(!is_annex(&[0x51, 0x01]));
413        assert!(!is_annex(&[]));
414    }
415
416    #[test]
417    fn test_create_annex() {
418        let annex = create_annex(&[0x01, 0x02]);
419        assert_eq!(annex, vec![0x50, 0x01, 0x02]);
420        assert!(is_annex(&annex));
421    }
422
423    #[test]
424    fn test_sighash_type_parsing() {
425        assert_eq!(SighashType::from_byte(0x00), Some(SighashType::Default));
426        assert_eq!(SighashType::from_byte(0x01), Some(SighashType::All));
427        assert_eq!(
428            SighashType::from_byte(0x81),
429            Some(SighashType::AllAnyoneCanPay)
430        );
431        assert_eq!(SighashType::from_byte(0xFF), None);
432    }
433
434    #[test]
435    fn test_script_hash() {
436        let s1 = Script::new().push_opcode(Opcode::OP_CHECKSIG);
437        let s2 = Script::new().push_opcode(Opcode::OP_CHECKSIG);
438        assert_eq!(s1.script_hash(), s2.script_hash());
439
440        let s3 = Script::new().push_opcode(Opcode::OP_RETURN);
441        assert_ne!(s1.script_hash(), s3.script_hash());
442    }
443
444    #[test]
445    fn test_script_into_bytes() {
446        let script = Script::new()
447            .push_opcode(Opcode::OP_1)
448            .push_opcode(Opcode::OP_CHECKSIG);
449        let bytes = script.into_bytes();
450        assert_eq!(bytes, vec![0x51, 0xac]);
451    }
452
453    #[test]
454    fn test_checksig_script_template() {
455        let key = [0xBB; 32];
456        let script = checksig_script(&key);
457        // 32 (push len) + 32 (key) + 1 (OP_CHECKSIG) = 34 + 1 = depends on encoding
458        assert_eq!(script.len(), 34);
459    }
460}