Skip to main content

ark_core/
script.rs

1use bitcoin::hashes::sha256;
2use bitcoin::hashes::Hash;
3use bitcoin::opcodes::all::*;
4use bitcoin::script::Instruction;
5use bitcoin::taproot::TaprootSpendInfo;
6use bitcoin::ScriptBuf;
7use bitcoin::XOnlyPublicKey;
8use std::fmt;
9
10/// A hash-lock script for ArkNotes.
11///
12/// The script checks that `SHA256(witness) == preimage_hash`.
13/// Anyone who knows the preimage can spend by providing it as the witness.
14pub fn arknote_script(preimage_hash: &sha256::Hash) -> ScriptBuf {
15    ScriptBuf::builder()
16        .push_opcode(OP_SHA256)
17        .push_slice(preimage_hash.as_byte_array())
18        .push_opcode(OP_EQUAL)
19        .into_script()
20}
21
22/// A conventional 2-of-2 multisignature [`ScriptBuf`].
23pub fn multisig_script(pk_0: XOnlyPublicKey, pk_1: XOnlyPublicKey) -> ScriptBuf {
24    ScriptBuf::builder()
25        .push_x_only_key(&pk_0)
26        .push_opcode(OP_CHECKSIGVERIFY)
27        .push_x_only_key(&pk_1)
28        .push_opcode(OP_CHECKSIG)
29        .into_script()
30}
31
32/// A 3-of-3 multisignature [`ScriptBuf`].
33///
34/// All three parties must sign to spend: `<pk_0> CHECKSIGVERIFY <pk_1> CHECKSIGVERIFY <pk_2>
35/// CHECKSIG`.
36pub fn multisig_3_of_3_script(
37    pk_0: XOnlyPublicKey,
38    pk_1: XOnlyPublicKey,
39    pk_2: XOnlyPublicKey,
40) -> ScriptBuf {
41    ScriptBuf::builder()
42        .push_x_only_key(&pk_0)
43        .push_opcode(OP_CHECKSIGVERIFY)
44        .push_x_only_key(&pk_1)
45        .push_opcode(OP_CHECKSIGVERIFY)
46        .push_x_only_key(&pk_2)
47        .push_opcode(OP_CHECKSIG)
48        .into_script()
49}
50
51/// A [`ScriptBuf`] allowing the owner of `pk` to spend after `locktime_seconds` have passed from
52/// the time the corresponding output was included in a block.
53// TODO: Should support multisig.
54pub fn csv_sig_script(locktime: bitcoin::Sequence, pk: XOnlyPublicKey) -> ScriptBuf {
55    ScriptBuf::builder()
56        .push_int(locktime.to_consensus_u32() as i64)
57        .push_opcode(OP_CSV)
58        .push_opcode(OP_DROP)
59        .push_x_only_key(&pk)
60        .push_opcode(OP_CHECKSIG)
61        .into_script()
62}
63
64/// The script pubkey for the Taproot output corresponding to the given [`TaprootSpendInfo`].
65pub fn tr_script_pubkey(spend_info: &TaprootSpendInfo) -> ScriptBuf {
66    let output_key = spend_info.output_key();
67    let builder = bitcoin::blockdata::script::Builder::new();
68    builder
69        .push_opcode(OP_PUSHNUM_1)
70        .push_slice(output_key.serialize())
71        .into_script()
72}
73
74/// Extracts all [`XOnlyPublicKey`]s from checksig patterns in the script.
75///
76/// Finds all 32-byte data pushes that are immediately followed by
77/// [`OP_CHECKSIG`] or [`OP_CHECKSIGVERIFY`] opcodes.
78///
79/// Returns an empty vector if no matching keys are found.
80pub fn extract_checksig_pubkeys(script: &ScriptBuf) -> Vec<XOnlyPublicKey> {
81    let instructions: Vec<_> = script.instructions().filter_map(|inst| inst.ok()).collect();
82
83    let mut pubkeys = Vec::new();
84
85    for window in instructions.windows(2) {
86        let (push, checksig) = (&window[0], &window[1]);
87
88        // Check if we have a 32-byte push followed by CHECKSIG or CHECKSIGVERIFY
89        if let Instruction::PushBytes(bytes) = push {
90            if bytes.len() != 32 {
91                continue;
92            }
93
94            let is_checksig = matches!(
95                checksig,
96                Instruction::Op(op) if *op == OP_CHECKSIG || *op == OP_CHECKSIGVERIFY
97            );
98
99            if let Ok(pk) = XOnlyPublicKey::from_slice(bytes.as_bytes()) {
100                if is_checksig {
101                    pubkeys.push(pk);
102                }
103            }
104        }
105    }
106
107    pubkeys
108}
109
110pub fn extract_sequence_from_csv_sig_script(
111    script: &ScriptBuf,
112) -> Result<bitcoin::Sequence, InvalidCsvSigScriptError> {
113    let csv_index = script
114        .to_bytes()
115        .windows(2)
116        .position(|window| *window == [OP_CSV.to_u8(), OP_DROP.to_u8()])
117        .ok_or(InvalidCsvSigScriptError)?;
118
119    let before_csv = &script.to_bytes()[..csv_index];
120
121    // It is either `OP_PUSHNUM_X` (a single byte) or `OP_PUSH_BYTES_X BYTES` (more than one
122    // byte).
123    let sequence = if before_csv.len() > 1 {
124        &before_csv[1..]
125    } else {
126        before_csv
127    };
128
129    let mut sequence = sequence.to_vec();
130    sequence.reverse();
131
132    let mut buffer = [0u8; 4];
133    let input_len = sequence.len();
134    let start_index = 4 - input_len; // calculate how many spaces to leave at the front
135
136    buffer[start_index..].copy_from_slice(&sequence);
137
138    let sequence = u32::from_be_bytes(buffer);
139
140    let sequence = bitcoin::Sequence::from_consensus(sequence);
141
142    Ok(sequence)
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct InvalidCsvSigScriptError;
147
148impl fmt::Display for InvalidCsvSigScriptError {
149    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150        f.write_str("invalid CSV-Sig script")
151    }
152}
153
154impl std::error::Error for InvalidCsvSigScriptError {}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use bitcoin::locktime;
160    use bitcoin::XOnlyPublicKey;
161    use std::str::FromStr;
162
163    #[test]
164    fn test_extract_sequence_from_csv_sig_script() {
165        // Equivalent to two 512-second intervals.
166        let locktime_seconds = 1024;
167        let sequence = bitcoin::Sequence::from_seconds_ceil(locktime_seconds).unwrap();
168
169        let pk = XOnlyPublicKey::from_str(
170            "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
171        )
172        .unwrap();
173
174        let script = csv_sig_script(sequence, pk);
175
176        let parsed = extract_sequence_from_csv_sig_script(&script).unwrap();
177        let parsed = parsed.to_relative_lock_time();
178
179        assert_eq!(
180            parsed,
181            locktime::relative::LockTime::from_512_second_intervals(2).into()
182        );
183    }
184
185    #[test]
186    fn test_multisig_3_of_3_script() {
187        let pk_0 = XOnlyPublicKey::from_str(
188            "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
189        )
190        .unwrap();
191        let pk_1 = XOnlyPublicKey::from_str(
192            "28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
193        )
194        .unwrap();
195        let pk_2 = XOnlyPublicKey::from_str(
196            "38845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
197        )
198        .unwrap();
199
200        let script = multisig_3_of_3_script(pk_0, pk_1, pk_2);
201        let pubkeys = extract_checksig_pubkeys(&script);
202
203        assert_eq!(pubkeys.len(), 3);
204        assert_eq!(pubkeys[0], pk_0);
205        assert_eq!(pubkeys[1], pk_1);
206        assert_eq!(pubkeys[2], pk_2);
207    }
208
209    #[test]
210    fn test_extract_checksig_pubkeys_from_multisig() {
211        let pk_0 = XOnlyPublicKey::from_str(
212            "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
213        )
214        .unwrap();
215        let pk_1 = XOnlyPublicKey::from_str(
216            "28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
217        )
218        .unwrap();
219
220        let script = multisig_script(pk_0, pk_1);
221        let pubkeys = extract_checksig_pubkeys(&script);
222
223        assert_eq!(pubkeys.len(), 2);
224        assert_eq!(pubkeys[0], pk_0);
225        assert_eq!(pubkeys[1], pk_1);
226    }
227
228    #[test]
229    fn test_extract_checksig_pubkeys_from_csv_sig() {
230        let pk = XOnlyPublicKey::from_str(
231            "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
232        )
233        .unwrap();
234        let sequence = bitcoin::Sequence::from_seconds_ceil(1024).unwrap();
235
236        let script = csv_sig_script(sequence, pk);
237        let pubkeys = extract_checksig_pubkeys(&script);
238
239        assert_eq!(pubkeys.len(), 1);
240        assert_eq!(pubkeys[0], pk);
241    }
242
243    #[test]
244    fn test_extract_checksig_pubkeys_empty_script() {
245        let script = ScriptBuf::new();
246        let pubkeys = extract_checksig_pubkeys(&script);
247
248        assert!(pubkeys.is_empty());
249    }
250
251    #[test]
252    fn test_extract_checksig_pubkeys_no_checksig() {
253        // Script with only OP_DROP and OP_RETURN, no checksig
254        let script = ScriptBuf::builder()
255            .push_opcode(OP_DROP)
256            .push_opcode(OP_RETURN)
257            .into_script();
258
259        let pubkeys = extract_checksig_pubkeys(&script);
260
261        assert!(pubkeys.is_empty());
262    }
263}