rustywallet_multisig/
script.rs

1//! Multisig script generation.
2
3use crate::config::MultisigConfig;
4
5/// Bitcoin script opcodes.
6pub mod opcodes {
7    pub const OP_0: u8 = 0x00;
8    pub const OP_1: u8 = 0x51;
9    pub const OP_16: u8 = 0x60;
10    pub const OP_CHECKMULTISIG: u8 = 0xae;
11    pub const OP_HASH160: u8 = 0xa9;
12    pub const OP_EQUAL: u8 = 0x87;
13}
14
15/// Build a multisig redeem script.
16///
17/// Format: `OP_M <pubkey1> <pubkey2> ... <pubkeyN> OP_N OP_CHECKMULTISIG`
18pub fn build_multisig_script(config: &MultisigConfig) -> Vec<u8> {
19    let mut script = Vec::new();
20
21    // OP_M (threshold)
22    script.push(number_to_opcode(config.threshold()));
23
24    // Push each public key
25    for pubkey in config.public_keys() {
26        script.push(33); // Push 33 bytes
27        script.extend_from_slice(pubkey);
28    }
29
30    // OP_N (total keys)
31    script.push(number_to_opcode(config.total()));
32
33    // OP_CHECKMULTISIG
34    script.push(opcodes::OP_CHECKMULTISIG);
35
36    script
37}
38
39/// Build P2SH scriptPubKey from script hash.
40///
41/// Format: `OP_HASH160 <20-byte-hash> OP_EQUAL`
42pub fn build_p2sh_script_pubkey(script_hash: &[u8; 20]) -> Vec<u8> {
43    let mut script = Vec::with_capacity(23);
44    script.push(opcodes::OP_HASH160);
45    script.push(20); // Push 20 bytes
46    script.extend_from_slice(script_hash);
47    script.push(opcodes::OP_EQUAL);
48    script
49}
50
51/// Build P2WSH scriptPubKey from witness script hash.
52///
53/// Format: `OP_0 <32-byte-hash>`
54pub fn build_p2wsh_script_pubkey(script_hash: &[u8; 32]) -> Vec<u8> {
55    let mut script = Vec::with_capacity(34);
56    script.push(opcodes::OP_0);
57    script.push(32); // Push 32 bytes
58    script.extend_from_slice(script_hash);
59    script
60}
61
62/// Build P2SH-P2WSH redeem script (wraps P2WSH in P2SH).
63///
64/// Format: `OP_0 <32-byte-witness-script-hash>`
65pub fn build_p2sh_p2wsh_redeem_script(witness_script_hash: &[u8; 32]) -> Vec<u8> {
66    build_p2wsh_script_pubkey(witness_script_hash)
67}
68
69/// Convert a number (1-16) to its opcode representation.
70fn number_to_opcode(n: u8) -> u8 {
71    if n == 0 {
72        opcodes::OP_0
73    } else if n <= 16 {
74        opcodes::OP_1 + n - 1
75    } else {
76        // For numbers > 16, we'd need to push the value
77        // But multisig is limited to 15 keys, so this shouldn't happen
78        panic!("Number too large for opcode: {}", n);
79    }
80}
81
82/// Parse M and N from a multisig redeem script.
83pub fn parse_multisig_script(script: &[u8]) -> Option<(u8, u8, Vec<[u8; 33]>)> {
84    if script.len() < 3 {
85        return None;
86    }
87
88    // First byte should be OP_M
89    let m = opcode_to_number(script[0])?;
90    if m == 0 || m > 15 {
91        return None;
92    }
93
94    // Parse public keys
95    let mut pos = 1;
96    let mut pubkeys = Vec::new();
97
98    while pos < script.len() {
99        let byte = script[pos];
100        
101        // Check if this is OP_N (end of pubkeys)
102        if (opcodes::OP_1..=opcodes::OP_16).contains(&byte) {
103            break;
104        }
105
106        // Should be a push of 33 bytes
107        if byte != 33 {
108            return None;
109        }
110
111        if pos + 34 > script.len() {
112            return None;
113        }
114
115        let mut pubkey = [0u8; 33];
116        pubkey.copy_from_slice(&script[pos + 1..pos + 34]);
117        pubkeys.push(pubkey);
118        pos += 34;
119    }
120
121    if pos >= script.len() {
122        return None;
123    }
124
125    // Parse OP_N
126    let n = opcode_to_number(script[pos])?;
127    if n as usize != pubkeys.len() {
128        return None;
129    }
130
131    // Check OP_CHECKMULTISIG
132    if pos + 1 >= script.len() || script[pos + 1] != opcodes::OP_CHECKMULTISIG {
133        return None;
134    }
135
136    Some((m, n, pubkeys))
137}
138
139/// Convert an opcode to its number value.
140fn opcode_to_number(opcode: u8) -> Option<u8> {
141    if opcode == opcodes::OP_0 {
142        Some(0)
143    } else if (opcodes::OP_1..=opcodes::OP_16).contains(&opcode) {
144        Some(opcode - opcodes::OP_1 + 1)
145    } else {
146        None
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::config::MultisigConfig;
154
155    fn make_pubkey(seed: u8) -> [u8; 33] {
156        let mut key = [seed; 33];
157        key[0] = 0x02;
158        key
159    }
160
161    #[test]
162    fn test_build_2_of_3_script() {
163        let keys = vec![make_pubkey(1), make_pubkey(2), make_pubkey(3)];
164        let config = MultisigConfig::new(2, keys).unwrap();
165        let script = build_multisig_script(&config);
166
167        // OP_2 + 3*(1+33) + OP_3 + OP_CHECKMULTISIG = 1 + 102 + 1 + 1 = 105
168        assert_eq!(script.len(), 105);
169        assert_eq!(script[0], opcodes::OP_1 + 1); // OP_2
170        assert_eq!(script[script.len() - 2], opcodes::OP_1 + 2); // OP_3
171        assert_eq!(script[script.len() - 1], opcodes::OP_CHECKMULTISIG);
172    }
173
174    #[test]
175    fn test_parse_multisig_script() {
176        let keys = vec![make_pubkey(1), make_pubkey(2), make_pubkey(3)];
177        let config = MultisigConfig::new(2, keys.clone()).unwrap();
178        let script = build_multisig_script(&config);
179
180        let (m, n, parsed_keys) = parse_multisig_script(&script).unwrap();
181        assert_eq!(m, 2);
182        assert_eq!(n, 3);
183        assert_eq!(parsed_keys.len(), 3);
184    }
185
186    #[test]
187    fn test_p2sh_script_pubkey() {
188        let hash = [0xab; 20];
189        let script = build_p2sh_script_pubkey(&hash);
190        
191        assert_eq!(script.len(), 23);
192        assert_eq!(script[0], opcodes::OP_HASH160);
193        assert_eq!(script[1], 20);
194        assert_eq!(script[22], opcodes::OP_EQUAL);
195    }
196
197    #[test]
198    fn test_p2wsh_script_pubkey() {
199        let hash = [0xcd; 32];
200        let script = build_p2wsh_script_pubkey(&hash);
201        
202        assert_eq!(script.len(), 34);
203        assert_eq!(script[0], opcodes::OP_0);
204        assert_eq!(script[1], 32);
205    }
206}