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