Skip to main content

chains_sdk/bitcoin/
miniscript.rs

1//! Miniscript — A structured representation of Bitcoin Script spending conditions.
2//!
3//! Provides types for expressing spending policies and compiling them to
4//! Bitcoin Script. Supports satisfaction analysis (witness size estimation).
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::bitcoin::miniscript::*;
9//!
10//! let key1 = [0x02u8; 33];
11//! let key2 = [0x03u8; 33];
12//!
13//! // 2-of-2 multisig policy
14//! let policy = Policy::Threshold(2, vec![
15//!     Policy::Key(key1),
16//!     Policy::Key(key2),
17//! ]);
18//! let ms = policy.compile().unwrap();
19//! let script = ms.encode();
20//! ```
21
22use crate::error::SignerError;
23
24// ═══════════════════════════════════════════════════════════════════
25// Opcodes
26// ═══════════════════════════════════════════════════════════════════
27
28#[allow(dead_code)]
29mod op {
30    pub const OP_0: u8 = 0x00;
31    pub const OP_IF: u8 = 0x63;
32    pub const OP_NOTIF: u8 = 0x64;
33    pub const OP_ELSE: u8 = 0x67;
34    pub const OP_ENDIF: u8 = 0x68;
35    pub const OP_VERIFY: u8 = 0x69;
36    pub const OP_RETURN: u8 = 0x6A;
37    pub const OP_TOALTSTACK: u8 = 0x6B;
38    pub const OP_FROMALTSTACK: u8 = 0x6C;
39    pub const OP_IFDUP: u8 = 0x73;
40    pub const OP_DUP: u8 = 0x76;
41    pub const OP_SWAP: u8 = 0x7C;
42    pub const OP_EQUAL: u8 = 0x87;
43    pub const OP_EQUALVERIFY: u8 = 0x88;
44    pub const OP_SIZE: u8 = 0x82;
45    pub const OP_BOOLOR: u8 = 0x9B;
46    pub const OP_ADD: u8 = 0x93;
47    pub const OP_HASH160: u8 = 0xA9;
48    pub const OP_SHA256: u8 = 0xA8;
49    pub const OP_RIPEMD160: u8 = 0xA6;
50    pub const OP_HASH256: u8 = 0xAA;
51    pub const OP_CHECKSIG: u8 = 0xAC;
52    pub const OP_CHECKSIGVERIFY: u8 = 0xAD;
53    pub const OP_CHECKMULTISIG: u8 = 0xAE;
54    pub const OP_CHECKMULTISIGVERIFY: u8 = 0xAF;
55    pub const OP_CLTV: u8 = 0xB1;
56    pub const OP_CSV: u8 = 0xB2;
57}
58
59// ═══════════════════════════════════════════════════════════════════
60// Policy — Human-readable spending conditions
61// ═══════════════════════════════════════════════════════════════════
62
63/// A high-level spending policy.
64///
65/// Policies describe *what* conditions must be satisfied, without specifying
66/// *how* they are encoded in Bitcoin Script.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Policy {
69    /// Require a signature for this public key (33-byte compressed).
70    Key([u8; 33]),
71    /// Absolute timelock (BIP-65 CLTV).
72    After(u32),
73    /// Relative timelock (BIP-112 CSV).
74    Older(u32),
75    /// SHA-256 hash preimage lock.
76    Sha256([u8; 32]),
77    /// RIPEMD-160 hash preimage lock.
78    Ripemd160([u8; 20]),
79    /// HASH-160 (SHA-256 → RIPEMD-160) preimage lock.
80    Hash160([u8; 20]),
81    /// All sub-policies must be satisfied (AND).
82    And(Vec<Policy>),
83    /// Any one sub-policy must be satisfied (OR with equal weights).
84    Or(Vec<Policy>),
85    /// k-of-n threshold: at least `k` sub-policies must be satisfied.
86    Threshold(usize, Vec<Policy>),
87    /// Always fails (un-spendable).
88    Unsatisfiable,
89    /// Always succeeds (trivially satisfiable).
90    Trivial,
91}
92
93#[allow(clippy::expect_used)]
94impl Policy {
95    /// Compile this policy to a Miniscript fragment.
96    ///
97    /// Returns `Err` if the policy is malformed (e.g., empty threshold,
98    /// threshold k > n, or empty AND/OR).
99    pub fn compile(&self) -> Result<Miniscript, SignerError> {
100        match self {
101            Policy::Key(pk) => Ok(Miniscript::Pk(*pk)),
102            Policy::After(n) => Ok(Miniscript::After(*n)),
103            Policy::Older(n) => Ok(Miniscript::Older(*n)),
104            Policy::Sha256(h) => Ok(Miniscript::Sha256(*h)),
105            Policy::Ripemd160(h) => Ok(Miniscript::Ripemd160(*h)),
106            Policy::Hash160(h) => Ok(Miniscript::Hash160(*h)),
107            Policy::Unsatisfiable => Ok(Miniscript::False),
108            Policy::Trivial => Ok(Miniscript::True),
109
110            Policy::And(subs) => {
111                if subs.is_empty() {
112                    return Err(SignerError::ParseError("empty AND policy".into()));
113                }
114                let compiled: Vec<Miniscript> =
115                    subs.iter()
116                        .map(|p| p.compile())
117                        .collect::<Result<Vec<_>, _>>()?;
118                let mut iter = compiled.into_iter();
119                let first = iter.next().expect("non-empty");
120                Ok(iter.fold(first, |acc, ms| {
121                    Miniscript::AndV(Box::new(acc), Box::new(ms))
122                }))
123            }
124
125            Policy::Or(subs) => {
126                if subs.is_empty() {
127                    return Err(SignerError::ParseError("empty OR policy".into()));
128                }
129                let compiled: Vec<Miniscript> =
130                    subs.iter()
131                        .map(|p| p.compile())
132                        .collect::<Result<Vec<_>, _>>()?;
133                let mut iter = compiled.into_iter();
134                let first = iter.next().expect("non-empty");
135                Ok(iter.fold(first, |acc, ms| {
136                    Miniscript::OrB(Box::new(acc), Box::new(ms))
137                }))
138            }
139
140            Policy::Threshold(k, subs) => {
141                let k = *k;
142                if subs.is_empty() {
143                    return Err(SignerError::ParseError("empty threshold".into()));
144                }
145                if k == 0 || k > subs.len() {
146                    return Err(SignerError::ParseError(format!(
147                        "invalid threshold {k} of {}",
148                        subs.len()
149                    )));
150                }
151
152                // Special cases: all keys → OP_CHECKMULTISIG
153                let all_keys = subs.iter().all(|p| matches!(p, Policy::Key(_)));
154                if all_keys && subs.len() <= 20 {
155                    let keys: Vec<[u8; 33]> = subs
156                        .iter()
157                        .map(|p| match p {
158                            Policy::Key(k) => *k,
159                            _ => unreachable!(),
160                        })
161                        .collect();
162                    return Ok(Miniscript::ThreshM(k, keys));
163                }
164
165                // General threshold compiled as and_v / or_b chains
166                if k == subs.len() {
167                    // All required → AND
168                    Policy::And(subs.clone()).compile()
169                } else if k == 1 {
170                    // Any one → OR
171                    Policy::Or(subs.clone()).compile()
172                } else {
173                    // General k-of-n: compile each sub and use Thresh
174                    let compiled: Vec<Miniscript> = subs
175                        .iter()
176                        .map(|p| p.compile())
177                        .collect::<Result<Vec<_>, _>>()?;
178                    Ok(Miniscript::Thresh(k, compiled))
179                }
180            }
181        }
182    }
183}
184
185// ═══════════════════════════════════════════════════════════════════
186// Miniscript — Script-level fragments
187// ═══════════════════════════════════════════════════════════════════
188
189/// A Miniscript fragment that maps directly to Bitcoin Script.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum Miniscript {
192    /// `<key> OP_CHECKSIG`
193    Pk([u8; 33]),
194    /// `OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG`
195    PkH([u8; 20]),
196    /// `<n> OP_CSV OP_VERIFY`
197    Older(u32),
198    /// `<n> OP_CLTV OP_VERIFY`
199    After(u32),
200    /// `OP_SIZE <32> OP_EQUALVERIFY OP_SHA256 <hash> OP_EQUAL`
201    Sha256([u8; 32]),
202    /// `OP_SIZE <32> OP_EQUALVERIFY OP_RIPEMD160 <hash> OP_EQUAL`
203    Ripemd160([u8; 20]),
204    /// `OP_SIZE <32> OP_EQUALVERIFY OP_HASH160 <hash> OP_EQUAL`
205    Hash160([u8; 20]),
206    /// `[X] [Y] OP_VERIFY` — both must succeed
207    AndV(Box<Miniscript>, Box<Miniscript>),
208    /// `[X] [Y] OP_BOOLOR` — either may succeed
209    OrB(Box<Miniscript>, Box<Miniscript>),
210    /// `OP_IF [X] OP_ELSE [Y] OP_ENDIF` — branching OR
211    OrI(Box<Miniscript>, Box<Miniscript>),
212    /// `<k> <key1> ... <keyn> <n> OP_CHECKMULTISIG`
213    ThreshM(usize, Vec<[u8; 33]>),
214    /// General threshold of sub-fragments with OP_ADD
215    Thresh(usize, Vec<Miniscript>),
216    /// `OP_1` — always true
217    True,
218    /// `OP_0` — always false
219    False,
220}
221
222impl Miniscript {
223    /// Encode this fragment to Bitcoin Script bytes.
224    #[must_use]
225    pub fn encode(&self) -> Vec<u8> {
226        let mut script = Vec::new();
227        self.encode_into(&mut script);
228        script
229    }
230
231    /// Encode into an existing buffer.
232    fn encode_into(&self, s: &mut Vec<u8>) {
233        match self {
234            Miniscript::Pk(key) => {
235                s.push(33); // push 33 bytes
236                s.extend_from_slice(key);
237                s.push(op::OP_CHECKSIG);
238            }
239
240            Miniscript::PkH(hash) => {
241                s.push(op::OP_DUP);
242                s.push(op::OP_HASH160);
243                s.push(20);
244                s.extend_from_slice(hash);
245                s.push(op::OP_EQUALVERIFY);
246                s.push(op::OP_CHECKSIG);
247            }
248
249            Miniscript::Older(n) => {
250                push_script_number(s, *n as i64);
251                s.push(op::OP_CSV);
252                s.push(op::OP_VERIFY);
253            }
254
255            Miniscript::After(n) => {
256                push_script_number(s, *n as i64);
257                s.push(op::OP_CLTV);
258                s.push(op::OP_VERIFY);
259            }
260
261            Miniscript::Sha256(hash) => {
262                s.push(op::OP_SIZE);
263                push_script_number(s, 32);
264                s.push(op::OP_EQUALVERIFY);
265                s.push(op::OP_SHA256);
266                s.push(32);
267                s.extend_from_slice(hash);
268                s.push(op::OP_EQUAL);
269            }
270
271            Miniscript::Ripemd160(hash) => {
272                s.push(op::OP_SIZE);
273                push_script_number(s, 32);
274                s.push(op::OP_EQUALVERIFY);
275                s.push(op::OP_RIPEMD160);
276                s.push(20);
277                s.extend_from_slice(hash);
278                s.push(op::OP_EQUAL);
279            }
280
281            Miniscript::Hash160(hash) => {
282                s.push(op::OP_SIZE);
283                push_script_number(s, 32);
284                s.push(op::OP_EQUALVERIFY);
285                s.push(op::OP_HASH160);
286                s.push(20);
287                s.extend_from_slice(hash);
288                s.push(op::OP_EQUAL);
289            }
290
291            Miniscript::AndV(left, right) => {
292                left.encode_into(s);
293                right.encode_into(s);
294            }
295
296            Miniscript::OrB(left, right) => {
297                left.encode_into(s);
298                right.encode_into(s);
299                s.push(op::OP_BOOLOR);
300            }
301
302            Miniscript::OrI(left, right) => {
303                s.push(op::OP_IF);
304                left.encode_into(s);
305                s.push(op::OP_ELSE);
306                right.encode_into(s);
307                s.push(op::OP_ENDIF);
308            }
309
310            Miniscript::ThreshM(k, keys) => {
311                push_script_number(s, *k as i64);
312                for key in keys {
313                    s.push(33);
314                    s.extend_from_slice(key);
315                }
316                push_script_number(s, keys.len() as i64);
317                s.push(op::OP_CHECKMULTISIG);
318            }
319
320            Miniscript::Thresh(k, subs) => {
321                if let Some((first, rest)) = subs.split_first() {
322                    first.encode_into(s);
323                    for sub in rest {
324                        sub.encode_into(s);
325                        s.push(op::OP_ADD);
326                    }
327                    push_script_number(s, *k as i64);
328                    s.push(op::OP_EQUAL);
329                }
330            }
331
332            Miniscript::True => {
333                s.push(0x51); // OP_1
334            }
335
336            Miniscript::False => {
337                s.push(op::OP_0);
338            }
339        }
340    }
341
342    /// Estimate the script size in bytes.
343    pub fn script_size(&self) -> usize {
344        self.encode().len()
345    }
346
347    /// Maximum number of witness stack elements needed to satisfy this fragment.
348    pub fn max_satisfaction_witness_elements(&self) -> usize {
349        match self {
350            Miniscript::Pk(_) => 1,                           // signature
351            Miniscript::PkH(_) => 2,                          // sig + pubkey
352            Miniscript::Older(_) | Miniscript::After(_) => 0, // sequence/locktime
353            Miniscript::Sha256(_) | Miniscript::Ripemd160(_) | Miniscript::Hash160(_) => 1, // preimage
354            Miniscript::AndV(l, r) => {
355                l.max_satisfaction_witness_elements() + r.max_satisfaction_witness_elements()
356            }
357            Miniscript::OrB(l, r) | Miniscript::OrI(l, r) => {
358                l.max_satisfaction_witness_elements()
359                    .max(r.max_satisfaction_witness_elements())
360                    + 1 // branch selector
361            }
362            Miniscript::ThreshM(k, _) => k + 1, // k sigs + dummy OP_0
363            Miniscript::Thresh(_, subs) => subs
364                .iter()
365                .map(|s| s.max_satisfaction_witness_elements())
366                .sum::<usize>(),
367            Miniscript::True | Miniscript::False => 0,
368        }
369    }
370
371    /// Maximum witness size in bytes (approximate).
372    pub fn max_satisfaction_size(&self) -> usize {
373        match self {
374            Miniscript::Pk(_) => 73,       // DER sig (72 max) + sighash byte
375            Miniscript::PkH(_) => 73 + 34, // sig + compact-push + pubkey
376            Miniscript::Older(_) | Miniscript::After(_) => 0,
377            Miniscript::Sha256(_) => 33, // 32-byte preimage + push
378            Miniscript::Ripemd160(_) | Miniscript::Hash160(_) => 33,
379            Miniscript::AndV(l, r) => l.max_satisfaction_size() + r.max_satisfaction_size(),
380            Miniscript::OrB(l, r) | Miniscript::OrI(l, r) => {
381                l.max_satisfaction_size().max(r.max_satisfaction_size()) + 1
382            }
383            Miniscript::ThreshM(k, _) => 1 + k * 73, // OP_0 + k signatures
384            Miniscript::Thresh(_, subs) => subs
385                .iter()
386                .map(|s| s.max_satisfaction_size())
387                .sum::<usize>(),
388            Miniscript::True | Miniscript::False => 0,
389        }
390    }
391}
392
393// ═══════════════════════════════════════════════════════════════════
394// Helpers
395// ═══════════════════════════════════════════════════════════════════
396
397/// Push a number in Bitcoin's minimal script number encoding.
398fn push_script_number(script: &mut Vec<u8>, n: i64) {
399    if n == 0 {
400        script.push(0x00); // OP_0
401        return;
402    }
403    if (1..=16).contains(&n) {
404        script.push(0x50 + n as u8); // OP_1..OP_16
405        return;
406    }
407
408    let negative = n < 0;
409    let mut abs_n = if negative { (-n) as u64 } else { n as u64 };
410    let mut bytes = Vec::new();
411
412    while abs_n > 0 {
413        bytes.push((abs_n & 0xFF) as u8);
414        abs_n >>= 8;
415    }
416
417    if bytes.last().is_some_and(|b| b & 0x80 != 0) {
418        bytes.push(if negative { 0x80 } else { 0x00 });
419    } else if negative {
420        let last = bytes.len() - 1;
421        bytes[last] |= 0x80;
422    }
423
424    script.push(bytes.len() as u8);
425    script.extend_from_slice(&bytes);
426}
427
428// ═══════════════════════════════════════════════════════════════════
429// Tests
430// ═══════════════════════════════════════════════════════════════════
431
432#[cfg(test)]
433#[allow(clippy::unwrap_used, clippy::expect_used)]
434mod tests {
435    use super::*;
436
437    const KEY1: [u8; 33] = [0x02; 33];
438    const KEY2: [u8; 33] = [0x03; 33];
439    const KEY3: [u8; 33] = [0x04; 33];
440    const HASH32: [u8; 32] = [0xAA; 32];
441    const HASH20: [u8; 20] = [0xBB; 20];
442
443    // ─── Policy Compilation ─────────────────────────────────────
444
445    #[test]
446    fn test_policy_key_compiles() {
447        let ms = Policy::Key(KEY1).compile().unwrap();
448        assert!(matches!(ms, Miniscript::Pk(_)));
449    }
450
451    #[test]
452    fn test_policy_after_compiles() {
453        let ms = Policy::After(500_000).compile().unwrap();
454        assert!(matches!(ms, Miniscript::After(500_000)));
455    }
456
457    #[test]
458    fn test_policy_older_compiles() {
459        let ms = Policy::Older(144).compile().unwrap();
460        assert!(matches!(ms, Miniscript::Older(144)));
461    }
462
463    #[test]
464    fn test_policy_sha256_compiles() {
465        let ms = Policy::Sha256(HASH32).compile().unwrap();
466        assert!(matches!(ms, Miniscript::Sha256(_)));
467    }
468
469    #[test]
470    fn test_policy_ripemd160_compiles() {
471        let ms = Policy::Ripemd160(HASH20).compile().unwrap();
472        assert!(matches!(ms, Miniscript::Ripemd160(_)));
473    }
474
475    #[test]
476    fn test_policy_hash160_compiles() {
477        let ms = Policy::Hash160(HASH20).compile().unwrap();
478        assert!(matches!(ms, Miniscript::Hash160(_)));
479    }
480
481    #[test]
482    fn test_policy_trivial() {
483        let ms = Policy::Trivial.compile().unwrap();
484        assert!(matches!(ms, Miniscript::True));
485    }
486
487    #[test]
488    fn test_policy_unsatisfiable() {
489        let ms = Policy::Unsatisfiable.compile().unwrap();
490        assert!(matches!(ms, Miniscript::False));
491    }
492
493    #[test]
494    fn test_policy_and_two_keys() {
495        let policy = Policy::And(vec![Policy::Key(KEY1), Policy::Key(KEY2)]);
496        let ms = policy.compile().unwrap();
497        assert!(matches!(ms, Miniscript::AndV(_, _)));
498    }
499
500    #[test]
501    fn test_policy_or_two_keys() {
502        let policy = Policy::Or(vec![Policy::Key(KEY1), Policy::Key(KEY2)]);
503        let ms = policy.compile().unwrap();
504        assert!(matches!(ms, Miniscript::OrB(_, _)));
505    }
506
507    #[test]
508    fn test_policy_threshold_all_keys() {
509        let policy = Policy::Threshold(
510            2,
511            vec![Policy::Key(KEY1), Policy::Key(KEY2), Policy::Key(KEY3)],
512        );
513        let ms = policy.compile().unwrap();
514        assert!(matches!(ms, Miniscript::ThreshM(2, _)));
515    }
516
517    #[test]
518    fn test_policy_threshold_n_of_n() {
519        let policy = Policy::Threshold(2, vec![Policy::Key(KEY1), Policy::Key(KEY2)]);
520        let ms = policy.compile().unwrap();
521        // 2-of-2 all keys → ThreshM
522        assert!(matches!(ms, Miniscript::ThreshM(2, _)));
523    }
524
525    // ─── Policy Errors ──────────────────────────────────────────
526
527    #[test]
528    fn test_policy_empty_and_errors() {
529        assert!(Policy::And(vec![]).compile().is_err());
530    }
531
532    #[test]
533    fn test_policy_empty_or_errors() {
534        assert!(Policy::Or(vec![]).compile().is_err());
535    }
536
537    #[test]
538    fn test_policy_empty_threshold_errors() {
539        assert!(Policy::Threshold(1, vec![]).compile().is_err());
540    }
541
542    #[test]
543    fn test_policy_threshold_k_zero_errors() {
544        assert!(Policy::Threshold(0, vec![Policy::Key(KEY1)])
545            .compile()
546            .is_err());
547    }
548
549    #[test]
550    fn test_policy_threshold_k_exceeds_n_errors() {
551        assert!(
552            Policy::Threshold(3, vec![Policy::Key(KEY1), Policy::Key(KEY2)])
553                .compile()
554                .is_err()
555        );
556    }
557
558    // ─── Script Encoding ────────────────────────────────────────
559
560    #[test]
561    fn test_pk_script_structure() {
562        let script = Miniscript::Pk(KEY1).encode();
563        assert_eq!(script[0], 33); // push 33
564        assert_eq!(&script[1..34], &KEY1);
565        assert_eq!(script[34], op::OP_CHECKSIG);
566        assert_eq!(script.len(), 35);
567    }
568
569    #[test]
570    fn test_pkh_script_structure() {
571        let script = Miniscript::PkH(HASH20).encode();
572        assert_eq!(script[0], op::OP_DUP);
573        assert_eq!(script[1], op::OP_HASH160);
574        assert_eq!(script[2], 20);
575        assert_eq!(&script[3..23], &HASH20);
576        assert_eq!(script[23], op::OP_EQUALVERIFY);
577        assert_eq!(script[24], op::OP_CHECKSIG);
578    }
579
580    #[test]
581    fn test_after_script_contains_cltv() {
582        let script = Miniscript::After(500_000).encode();
583        assert!(script.contains(&op::OP_CLTV));
584        assert!(script.contains(&op::OP_VERIFY));
585    }
586
587    #[test]
588    fn test_older_script_contains_csv() {
589        let script = Miniscript::Older(144).encode();
590        assert!(script.contains(&op::OP_CSV));
591        assert!(script.contains(&op::OP_VERIFY));
592    }
593
594    #[test]
595    fn test_sha256_script_contains_hash_op() {
596        let script = Miniscript::Sha256(HASH32).encode();
597        assert!(script.contains(&op::OP_SHA256));
598        assert!(script.contains(&op::OP_EQUAL));
599        assert!(script.contains(&op::OP_SIZE));
600    }
601
602    #[test]
603    fn test_ripemd160_script_contains_hash_op() {
604        let script = Miniscript::Ripemd160(HASH20).encode();
605        assert!(script.contains(&op::OP_RIPEMD160));
606    }
607
608    #[test]
609    fn test_hash160_script_contains_hash_op() {
610        let script = Miniscript::Hash160(HASH20).encode();
611        assert!(script.contains(&op::OP_HASH160));
612    }
613
614    #[test]
615    fn test_thresh_m_2_of_3_script() {
616        let script = Miniscript::ThreshM(2, vec![KEY1, KEY2, KEY3]).encode();
617        assert_eq!(script[0], 0x52); // OP_2
618                                     // 3 keys × (1 push + 33 bytes)
619        assert_eq!(script[1], 33);
620        assert_eq!(script[1 + 34], 33);
621        assert_eq!(script[1 + 68], 33);
622        // OP_3
623        assert_eq!(script[1 + 102], 0x53);
624        // OP_CHECKMULTISIG
625        assert_eq!(script[1 + 103], op::OP_CHECKMULTISIG);
626    }
627
628    #[test]
629    fn test_and_v_combines_scripts() {
630        let ms = Miniscript::AndV(
631            Box::new(Miniscript::Pk(KEY1)),
632            Box::new(Miniscript::Pk(KEY2)),
633        );
634        let script = ms.encode();
635        // Two pk scripts concatenated
636        assert_eq!(script.len(), 35 + 35); // 2 × pk script
637    }
638
639    #[test]
640    fn test_or_b_adds_boolor() {
641        let ms = Miniscript::OrB(
642            Box::new(Miniscript::Pk(KEY1)),
643            Box::new(Miniscript::Pk(KEY2)),
644        );
645        let script = ms.encode();
646        assert_eq!(*script.last().unwrap(), op::OP_BOOLOR);
647    }
648
649    #[test]
650    fn test_or_i_uses_if_else_endif() {
651        let ms = Miniscript::OrI(
652            Box::new(Miniscript::Pk(KEY1)),
653            Box::new(Miniscript::Pk(KEY2)),
654        );
655        let script = ms.encode();
656        assert_eq!(script[0], op::OP_IF);
657        assert!(script.contains(&op::OP_ELSE));
658        assert_eq!(*script.last().unwrap(), op::OP_ENDIF);
659    }
660
661    #[test]
662    fn test_true_encodes_op_1() {
663        let script = Miniscript::True.encode();
664        assert_eq!(script, vec![0x51]);
665    }
666
667    #[test]
668    fn test_false_encodes_op_0() {
669        let script = Miniscript::False.encode();
670        assert_eq!(script, vec![op::OP_0]);
671    }
672
673    // ─── Script Size ────────────────────────────────────────────
674
675    #[test]
676    fn test_pk_script_size() {
677        assert_eq!(Miniscript::Pk(KEY1).script_size(), 35);
678    }
679
680    #[test]
681    fn test_thresh_m_script_size() {
682        let ms = Miniscript::ThreshM(2, vec![KEY1, KEY2, KEY3]);
683        // OP_2 + 3×(push+key) + OP_3 + OP_CHECKMULTISIG = 1 + 102 + 1 + 1 = 105
684        assert_eq!(ms.script_size(), 105);
685    }
686
687    // ─── Witness Size ───────────────────────────────────────────
688
689    #[test]
690    fn test_pk_witness_elements() {
691        assert_eq!(Miniscript::Pk(KEY1).max_satisfaction_witness_elements(), 1);
692    }
693
694    #[test]
695    fn test_pkh_witness_elements() {
696        assert_eq!(
697            Miniscript::PkH(HASH20).max_satisfaction_witness_elements(),
698            2
699        );
700    }
701
702    #[test]
703    fn test_thresh_m_witness_elements() {
704        let ms = Miniscript::ThreshM(2, vec![KEY1, KEY2, KEY3]);
705        assert_eq!(ms.max_satisfaction_witness_elements(), 3); // 2 sigs + dummy OP_0
706    }
707
708    #[test]
709    fn test_pk_witness_size() {
710        assert_eq!(Miniscript::Pk(KEY1).max_satisfaction_size(), 73);
711    }
712
713    #[test]
714    fn test_thresh_m_witness_size() {
715        let ms = Miniscript::ThreshM(2, vec![KEY1, KEY2, KEY3]);
716        assert_eq!(ms.max_satisfaction_size(), 1 + 2 * 73); // OP_0 + 2 sigs
717    }
718
719    #[test]
720    fn test_sha256_witness_size() {
721        assert_eq!(Miniscript::Sha256(HASH32).max_satisfaction_size(), 33);
722    }
723
724    // ─── End-to-End ─────────────────────────────────────────────
725
726    #[test]
727    fn test_e2e_2_of_3_policy_to_script() {
728        let policy = Policy::Threshold(
729            2,
730            vec![Policy::Key(KEY1), Policy::Key(KEY2), Policy::Key(KEY3)],
731        );
732        let ms = policy.compile().unwrap();
733        let script = ms.encode();
734        // Should be a valid OP_CHECKMULTISIG script
735        assert_eq!(*script.last().unwrap(), op::OP_CHECKMULTISIG);
736        assert!(script.len() > 100);
737    }
738
739    #[test]
740    fn test_e2e_key_and_timelock() {
741        let policy = Policy::And(vec![Policy::Key(KEY1), Policy::After(800_000)]);
742        let ms = policy.compile().unwrap();
743        let script = ms.encode();
744        assert!(script.contains(&op::OP_CHECKSIG));
745        assert!(script.contains(&op::OP_CLTV));
746    }
747
748    #[test]
749    fn test_e2e_htlc_like_policy() {
750        // hashlock OR (key + timelock) — typical HTLC
751        let policy = Policy::Or(vec![
752            Policy::And(vec![Policy::Sha256(HASH32), Policy::Key(KEY1)]),
753            Policy::And(vec![Policy::Key(KEY2), Policy::After(700_000)]),
754        ]);
755        let ms = policy.compile().unwrap();
756        let script = ms.encode();
757        assert!(script.len() > 50);
758        assert!(script.contains(&op::OP_SHA256));
759        assert!(script.contains(&op::OP_CLTV));
760    }
761
762    // ─── push_script_number ─────────────────────────────────────
763
764    #[test]
765    fn test_push_script_number_zero() {
766        let mut s = Vec::new();
767        push_script_number(&mut s, 0);
768        assert_eq!(s, vec![0x00]);
769    }
770
771    #[test]
772    fn test_push_script_number_small() {
773        for n in 1..=16 {
774            let mut s = Vec::new();
775            push_script_number(&mut s, n);
776            assert_eq!(s, vec![0x50 + n as u8]);
777        }
778    }
779
780    #[test]
781    fn test_push_script_number_17() {
782        let mut s = Vec::new();
783        push_script_number(&mut s, 17);
784        assert_eq!(s, vec![1, 17]); // 1-byte push + value
785    }
786
787    #[test]
788    fn test_push_script_number_256() {
789        let mut s = Vec::new();
790        push_script_number(&mut s, 256);
791        assert_eq!(s, vec![2, 0x00, 0x01]); // 2-byte push LE
792    }
793}