bitcoin_explorer/parser/
script.rs

1use bitcoin::blockdata::opcodes::{all, All};
2use bitcoin::blockdata::script::Instruction;
3use bitcoin::hashes::{hash160, Hash};
4use bitcoin::util::address::Payload;
5use bitcoin::{Address, Network, PubkeyHash, PublicKey, Script};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use Instruction::{Op, PushBytes};
9
10///
11/// Different types of bitcoin Scripts.
12///
13/// See [An Analysis of Non-standard Transactions](https://doi.org/10.3389/fbloc.2019.00007)
14/// for a more detailed explanation.
15///
16#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub enum ScriptType {
18    OpReturn,
19    Pay2MultiSig,
20    Pay2PublicKey,
21    Pay2PublicKeyHash,
22    Pay2ScriptHash,
23    Pay2WitnessPublicKeyHash,
24    Pay2WitnessScriptHash,
25    WitnessProgram,
26    Unspendable,
27    NotRecognised,
28}
29
30///
31/// `ScriptInfo` stores a list of addresses extracted from ScriptPubKey.
32///
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct ScriptInfo {
35    pub addresses: Vec<Address>,
36    pub pattern: ScriptType,
37}
38
39///
40/// This function extract addresses and script type from Script.
41///
42pub fn evaluate_script(script: &Script, net: Network) -> ScriptInfo {
43    let address = Address::from_script(script, net);
44    if script.is_p2pk() {
45        ScriptInfo::new(p2pk_to_address(script), ScriptType::Pay2PublicKey)
46    } else if script.is_p2pkh() {
47        ScriptInfo::new(address, ScriptType::Pay2PublicKeyHash)
48    } else if script.is_p2sh() {
49        ScriptInfo::new(address, ScriptType::Pay2ScriptHash)
50    } else if script.is_v0_p2wpkh() {
51        ScriptInfo::new(address, ScriptType::Pay2WitnessPublicKeyHash)
52    } else if script.is_v0_p2wsh() {
53        ScriptInfo::new(address, ScriptType::Pay2WitnessScriptHash)
54    } else if script.is_witness_program() {
55        ScriptInfo::new(address, ScriptType::WitnessProgram)
56    } else if script.is_op_return() {
57        ScriptInfo::new(address, ScriptType::OpReturn)
58    } else if script.is_provably_unspendable() {
59        ScriptInfo::new(address, ScriptType::Unspendable)
60    } else if is_multisig(script) {
61        ScriptInfo::from_vec(multisig_addresses(script), ScriptType::Pay2MultiSig)
62    } else {
63        ScriptInfo::new(address, ScriptType::NotRecognised)
64    }
65}
66
67impl ScriptInfo {
68    pub(crate) fn new(address: Option<Address>, pattern: ScriptType) -> Self {
69        if let Some(address) = address {
70            Self::from_vec(vec![address], pattern)
71        } else {
72            Self::from_vec(Vec::new(), pattern)
73        }
74    }
75
76    pub(crate) fn from_vec(addresses: Vec<Address>, pattern: ScriptType) -> Self {
77        Self { addresses, pattern }
78    }
79}
80
81///
82/// translated from Bitcoinj:
83/// [isSentToMultisig()](https://github.com/bitcoinj/bitcoinj/blob/d3d5edbcbdb91b25de4df3b6ed6740d7e2329efc/core/src/main/java/org/bitcoinj/script/ScriptPattern.java#L225:L246)
84fn is_multisig(script: &Script) -> bool {
85    // Read OpCodes
86    let mut chunks: Vec<Instruction> = Vec::new();
87    for i in script.instructions() {
88        if let Ok(i) = i {
89            chunks.push(i);
90        } else {
91            return false;
92        }
93    }
94
95    // At least four chunks
96    if chunks.len() < 4 {
97        return false;
98    }
99
100    // Must end in OP_CHECKMULTISIG[VERIFY].
101    match chunks.last().unwrap() {
102        PushBytes(_) => {
103            return false;
104        }
105        Op(op) => {
106            if !(op.eq(&all::OP_CHECKMULTISIG) || op.eq(&all::OP_CHECKMULTISIGVERIFY)) {
107                return false;
108            }
109        }
110    }
111
112    // Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys).
113    let second_last_chunk = chunks.get(chunks.len() - 2).unwrap();
114    if let Some(num_keys) = get_num_keys(second_last_chunk) {
115        // check number of chunks
116        if num_keys < 1 || (num_keys + 3) as usize != chunks.len() {
117            return false;
118        }
119    } else {
120        return false;
121    }
122
123    // the rest must be data (except the first and the last 2)
124    for chunk in chunks.iter().skip(1).take(chunks.len() - 3) {
125        if let Op(_) = chunk {
126            return false;
127        }
128    }
129
130    // First chunk must be an OP_N opcode too.
131    if let Some(num_keys) = get_num_keys(chunks.first().unwrap()) {
132        // check number of chunks
133        if num_keys < 1 {
134            return false;
135        }
136    } else {
137        return false;
138    }
139    true
140}
141
142///
143/// Obtain addresses for multisig transactions.
144///
145fn multisig_addresses(script: &Script) -> Vec<Address> {
146    assert!(is_multisig(script));
147    let ops: Vec<Instruction> = script.instructions().filter_map(|o| o.ok()).collect();
148
149    // obtain number of keys
150    let num_keys = {
151        if let Some(Op(op)) = ops.get(ops.len() - 2) {
152            decode_from_op_n(op)
153        } else {
154            unreachable!()
155        }
156    };
157    // read public keys
158    let mut public_keys = Vec::with_capacity(num_keys as usize);
159    for op in ops.iter().skip(1).take(num_keys as usize) {
160        if let PushBytes(data) = op {
161            match PublicKey::from_slice(data) {
162                Ok(pk) => public_keys.push(Address {
163                    payload: Payload::PubkeyHash(pk.pubkey_hash()),
164                    network: Network::Bitcoin,
165                }),
166                Err(_) => return Vec::new(),
167            }
168        } else {
169            unreachable!()
170        }
171    }
172    public_keys
173}
174
175///
176/// Decode OP_N
177///
178/// translated from BitcoinJ:
179/// [decodeFromOpN()](https://github.com/bitcoinj/bitcoinj/blob/d3d5edbcbdb91b25de4df3b6ed6740d7e2329efc/core/src/main/java/org/bitcoinj/script/Script.java#L515:L524)
180///
181#[inline]
182fn decode_from_op_n(op: &All) -> i32 {
183    if op.eq(&all::OP_PUSHBYTES_0) {
184        0
185    } else if op.eq(&all::OP_PUSHNUM_NEG1) {
186        -1
187    } else {
188        op.into_u8() as i32 + 1 - all::OP_PUSHNUM_1.into_u8() as i32
189    }
190}
191
192///
193/// Get number of keys for multisig
194///
195#[inline]
196fn get_num_keys(op: &Instruction) -> Option<i32> {
197    match op {
198        PushBytes(_) => None,
199        Op(op) => {
200            if !(op.eq(&all::OP_PUSHNUM_NEG1)
201                || op.eq(&all::OP_PUSHBYTES_0)
202                || (op.ge(&all::OP_PUSHNUM_1) && all::OP_PUSHNUM_16.ge(op)))
203            {
204                None
205            } else {
206                Some(decode_from_op_n(op))
207            }
208        }
209    }
210}
211
212///
213/// Get address from p2pk script.
214///
215/// Can only be used for p2pk script,
216/// otherwise panic.
217///
218#[inline]
219fn p2pk_to_address(script: &Script) -> Option<Address> {
220    assert!(script.is_p2pk());
221    if let Some(Ok(Instruction::PushBytes(pk))) = script.instructions().next() {
222        // hash the 20 bytes public key
223        let pkh = hash160::Hash::hash(pk);
224        Some(Address {
225            payload: Payload::PubkeyHash(PubkeyHash::from_slice(&pkh).ok()?),
226            network: Network::Bitcoin,
227        })
228    } else {
229        unreachable!()
230    }
231}
232
233trait Cmp {
234    fn ge(&self, other: &Self) -> bool;
235}
236
237impl Cmp for bitcoin::blockdata::opcodes::All {
238    fn ge(&self, other: &Self) -> bool {
239        self.into_u8() >= other.into_u8()
240    }
241}
242
243impl fmt::Display for ScriptType {
244    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245        match *self {
246            ScriptType::OpReturn => write!(f, "OpReturn"),
247            ScriptType::Pay2MultiSig => write!(f, "Pay2MultiSig"),
248            ScriptType::Pay2PublicKey => write!(f, "Pay2PublicKey"),
249            ScriptType::Pay2PublicKeyHash => write!(f, "Pay2PublicKeyHash"),
250            ScriptType::Pay2ScriptHash => write!(f, "Pay2ScriptHash"),
251            ScriptType::Pay2WitnessPublicKeyHash => write!(f, "Pay2WitnessPublicKeyHash"),
252            ScriptType::Pay2WitnessScriptHash => write!(f, "Pay2WitnessScriptHash"),
253            ScriptType::WitnessProgram => write!(f, "WitnessProgram"),
254            ScriptType::Unspendable => write!(f, "Unspendable"),
255            ScriptType::NotRecognised => write!(f, "NotRecognised"),
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{evaluate_script, ScriptType};
263    use bitcoin::hashes::hex::{FromHex, ToHex};
264    use bitcoin::{Network, Script};
265
266    #[test]
267    fn test_bitcoin_script_p2pkh() {
268        // Raw output script: 76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac
269        //                    OP_DUP OP_HASH160 OP_PUSHDATA0(20 bytes) 12ab8dc588ca9d5787dde7eb29569da63c3a238c OP_EQUALVERIFY OP_CHECKSIG
270        let bytes = [
271            0x76 as u8, 0xa9, 0x14, 0x12, 0xab, 0x8d, 0xc5, 0x88, 0xca, 0x9d, 0x57, 0x87, 0xdd,
272            0xe7, 0xeb, 0x29, 0x56, 0x9d, 0xa6, 0x3c, 0x3a, 0x23, 0x8c, 0x88, 0xac,
273        ];
274        let result = evaluate_script(
275            &Script::from_hex(&bytes.to_hex()).unwrap(),
276            Network::Bitcoin,
277        );
278        assert_eq!(
279            result.addresses.get(0).unwrap().to_string(),
280            String::from("12higDjoCCNXSA95xZMWUdPvXNmkAduhWv")
281        );
282        assert_eq!(result.pattern, ScriptType::Pay2PublicKeyHash);
283    }
284
285    #[test]
286    fn test_bitcoin_script_p2pk() {
287        // https://blockchain.info/tx/e36f06a8dfe44c3d64be2d3fe56c77f91f6a39da4a5ffc086ecb5db9664e8583
288        // Raw output script: 0x41 0x044bca633a91de10df85a63d0a24cb09783148fe0e16c92e937fc4491580c860757148effa0595a955f44078b48ba67fa198782e8bb68115da0daa8fde5301f7f9 OP_CHECKSIG
289        //                    OP_PUSHDATA0(65 bytes) 0x04bdca... OP_CHECKSIG
290        let bytes = [
291            0x41 as u8, // Push next 65 bytes
292            0x04, 0x4b, 0xca, 0x63, 0x3a, 0x91, 0xde, 0x10, 0xdf, 0x85, 0xa6, 0x3d, 0x0a, 0x24,
293            0xcb, 0x09, 0x78, 0x31, 0x48, 0xfe, 0x0e, 0x16, 0xc9, 0x2e, 0x93, 0x7f, 0xc4, 0x49,
294            0x15, 0x80, 0xc8, 0x60, 0x75, 0x71, 0x48, 0xef, 0xfa, 0x05, 0x95, 0xa9, 0x55, 0xf4,
295            0x40, 0x78, 0xb4, 0x8b, 0xa6, 0x7f, 0xa1, 0x98, 0x78, 0x2e, 0x8b, 0xb6, 0x81, 0x15,
296            0xda, 0x0d, 0xaa, 0x8f, 0xde, 0x53, 0x01, 0xf7, 0xf9, 0xac,
297        ]; // OP_CHECKSIG
298        let result = evaluate_script(
299            &Script::from_hex(&bytes.to_hex()).unwrap(),
300            Network::Bitcoin,
301        );
302        assert_eq!(
303            result.addresses.get(0).unwrap().to_string(),
304            String::from("1LEWwJkDj8xriE87ALzQYcHjTmD8aqDj1f")
305        );
306        assert_eq!(result.pattern, ScriptType::Pay2PublicKey);
307    }
308
309    #[test]
310    fn test_bitcoin_script_p2ms() {
311        // 2-of-3 Multi sig output
312        // OP_2 33 0x022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da
313        // 33 0x03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9
314        // 33 0x021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18 OP_3 OP_CHECKMULTISIG
315        let bytes = [
316            0x52 as u8, 0x21, 0x02, 0x2d, 0xf8, 0x75, 0x04, 0x80, 0xad, 0x5b, 0x26, 0x95, 0x0b,
317            0x25, 0xc7, 0xba, 0x79, 0xd3, 0xe3, 0x7d, 0x75, 0xf6, 0x40, 0xf8, 0xe5, 0xd9, 0xbc,
318            0xd5, 0xb1, 0x50, 0xa0, 0xf8, 0x50, 0x14, 0xda, 0x21, 0x03, 0xe3, 0x81, 0x8b, 0x65,
319            0xbc, 0xc7, 0x3a, 0x7d, 0x64, 0x06, 0x41, 0x06, 0xa8, 0x59, 0xcc, 0x1a, 0x5a, 0x72,
320            0x8c, 0x43, 0x45, 0xff, 0x0b, 0x64, 0x12, 0x09, 0xfb, 0xa0, 0xd9, 0x0d, 0xe6, 0xe9,
321            0x21, 0x02, 0x1f, 0x2f, 0x6e, 0x1e, 0x50, 0xcb, 0x6a, 0x95, 0x39, 0x35, 0xc3, 0x60,
322            0x12, 0x84, 0x92, 0x5d, 0xec, 0xd3, 0xfd, 0x21, 0xbc, 0x44, 0x57, 0x12, 0x57, 0x68,
323            0x73, 0xfb, 0x8c, 0x6e, 0xbc, 0x18, 0x53, 0xae,
324        ];
325
326        let result = evaluate_script(
327            &Script::from_hex(&bytes.to_hex()).unwrap(),
328            Network::Bitcoin,
329        );
330        assert_eq!(result.pattern, ScriptType::Pay2MultiSig);
331    }
332
333    #[test]
334    fn test_bitcoin_script_p2sh() {
335        // Raw output script: a914e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a
336        //                    OP_HASH160 20 0xe9c3dd0c07aac76179ebc76a6c78d4d67c6c160a OP_EQUAL
337        let bytes = [
338            0xa9 as u8, 0x14, // OP_HASH160, OP_PUSHDATA0(20 bytes)
339            0xe9, 0xc3, 0xdd, 0x0c, 0x07, 0xaa, 0xc7, 0x61, 0x79, 0xeb, 0xc7, 0x6a, 0x6c, 0x78,
340            0xd4, 0xd6, 0x7c, 0x6c, 0x16, 0x0a, 0x87,
341        ]; // OP_EQUAL
342        let result = evaluate_script(
343            &Script::from_hex(&bytes.to_hex()).unwrap(),
344            Network::Bitcoin,
345        );
346        assert_eq!(
347            result.addresses.get(0).unwrap().to_string(),
348            String::from("3P14159f73E4gFr7JterCCQh9QjiTjiZrG")
349        );
350        assert_eq!(result.pattern, ScriptType::Pay2ScriptHash);
351    }
352
353    #[test]
354    fn test_bitcoin_script_non_standard() {
355        // Raw output script: 736372697074
356        //                    OP_IFDUP OP_IF OP_2SWAP OP_VERIFY OP_2OVER OP_DEPTH
357        let bytes = [0x73 as u8, 0x63, 0x72, 0x69, 0x70, 0x74];
358        let result = evaluate_script(
359            &Script::from_hex(&bytes.to_hex()).unwrap(),
360            Network::Bitcoin,
361        );
362        assert_eq!(result.addresses.get(0), None);
363        assert_eq!(result.pattern, ScriptType::NotRecognised);
364    }
365
366    #[test]
367    fn test_bitcoin_bogus_script() {
368        let bytes = [0x4c as u8, 0xFF, 0x00];
369        let result = evaluate_script(
370            &Script::from_hex(&bytes.to_hex()).unwrap(),
371            Network::Bitcoin,
372        );
373        assert_eq!(result.addresses.get(0), None);
374        assert_eq!(result.pattern, ScriptType::NotRecognised);
375    }
376}