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
148        .instructions()
149        .filter_map(|o| o.ok())
150        .collect();
151
152    // obtain number of keys
153    let num_keys = {
154        if let Some(Op(op)) = ops.get(ops.len() - 2) {
155            decode_from_op_n(op)
156        } else {
157            unreachable!()
158        }
159    };
160    // read public keys
161    let mut public_keys = Vec::with_capacity(num_keys as usize);
162    for op in ops.iter().skip(1).take(num_keys as usize) {
163        if let PushBytes(data) = op {
164            match PublicKey::from_slice(data) {
165                Ok(pk) => public_keys.push(Address {
166                    payload: Payload::PubkeyHash(pk.pubkey_hash()),
167                    network: Network::Bitcoin,
168                }),
169                Err(_) => return Vec::new(),
170            }
171        } else {
172            unreachable!()
173        }
174    }
175    public_keys
176}
177
178///
179/// Decode OP_N
180///
181/// translated from BitcoinJ:
182/// [decodeFromOpN()](https://github.com/bitcoinj/bitcoinj/blob/d3d5edbcbdb91b25de4df3b6ed6740d7e2329efc/core/src/main/java/org/bitcoinj/script/Script.java#L515:L524)
183///
184#[inline]
185fn decode_from_op_n(op: &All) -> i32 {
186    if op.eq(&all::OP_PUSHBYTES_0) {
187        0
188    } else if op.eq(&all::OP_PUSHNUM_NEG1) {
189        -1
190    } else {
191        op.into_u8() as i32 + 1 - all::OP_PUSHNUM_1.into_u8() as i32
192    }
193}
194
195///
196/// Get number of keys for multisig
197///
198#[inline]
199fn get_num_keys(op: &Instruction) -> Option<i32> {
200    match op {
201        PushBytes(_) => None,
202        Op(op) => {
203            if !(op.eq(&all::OP_PUSHNUM_NEG1)
204                || op.eq(&all::OP_PUSHBYTES_0)
205                || (op.ge(&all::OP_PUSHNUM_1) && all::OP_PUSHNUM_16.ge(op)))
206            {
207                None
208            } else {
209                Some(decode_from_op_n(op))
210            }
211        }
212    }
213}
214
215///
216/// Get address from p2pk script.
217///
218/// Can only be used for p2pk script,
219/// otherwise panic.
220///
221#[inline]
222fn p2pk_to_address(script: &Script) -> Option<Address> {
223    assert!(script.is_p2pk());
224    if let Some(Ok(Instruction::PushBytes(pk))) = script.instructions().next() {
225        // hash the 20 bytes public key
226        let pkh = hash160::Hash::hash(pk);
227        Some(Address {
228            payload: Payload::PubkeyHash(PubkeyHash::from_slice(&pkh).ok()?),
229            network: Network::Bitcoin,
230        })
231    } else {
232        unreachable!()
233    }
234}
235
236trait Cmp {
237    fn ge(&self, other: &Self) -> bool;
238}
239
240impl Cmp for bitcoin::blockdata::opcodes::All {
241    fn ge(&self, other: &Self) -> bool {
242        self.into_u8() >= other.into_u8()
243    }
244}
245
246impl fmt::Display for ScriptType {
247    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
248        match *self {
249            ScriptType::OpReturn => write!(f, "OpReturn"),
250            ScriptType::Pay2MultiSig => write!(f, "Pay2MultiSig"),
251            ScriptType::Pay2PublicKey => write!(f, "Pay2PublicKey"),
252            ScriptType::Pay2PublicKeyHash => write!(f, "Pay2PublicKeyHash"),
253            ScriptType::Pay2ScriptHash => write!(f, "Pay2ScriptHash"),
254            ScriptType::Pay2WitnessPublicKeyHash => write!(f, "Pay2WitnessPublicKeyHash"),
255            ScriptType::Pay2WitnessScriptHash => write!(f, "Pay2WitnessScriptHash"),
256            ScriptType::WitnessProgram => write!(f, "WitnessProgram"),
257            ScriptType::Unspendable => write!(f, "Unspendable"),
258            ScriptType::NotRecognised => write!(f, "NotRecognised"),
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::{evaluate_script, ScriptType};
266    use bitcoin::hashes::hex::{FromHex, ToHex};
267    use bitcoin::{Network, Script};
268
269    #[test]
270    fn test_bitcoin_script_p2pkh() {
271        // Raw output script: 76a91412ab8dc588ca9d5787dde7eb29569da63c3a238c88ac
272        //                    OP_DUP OP_HASH160 OP_PUSHDATA0(20 bytes) 12ab8dc588ca9d5787dde7eb29569da63c3a238c OP_EQUALVERIFY OP_CHECKSIG
273        let bytes = [
274            0x76 as u8, 0xa9, 0x14, 0x12, 0xab, 0x8d, 0xc5, 0x88, 0xca, 0x9d, 0x57, 0x87, 0xdd,
275            0xe7, 0xeb, 0x29, 0x56, 0x9d, 0xa6, 0x3c, 0x3a, 0x23, 0x8c, 0x88, 0xac,
276        ];
277        let result = evaluate_script(
278            &Script::from_hex(&bytes.to_hex()).unwrap(),
279            Network::Bitcoin,
280        );
281        assert_eq!(
282            result.addresses.get(0).unwrap().to_string(),
283            String::from("12higDjoCCNXSA95xZMWUdPvXNmkAduhWv")
284        );
285        assert_eq!(result.pattern, ScriptType::Pay2PublicKeyHash);
286    }
287
288    #[test]
289    fn test_bitcoin_script_p2pk() {
290        // https://blockchain.info/tx/e36f06a8dfe44c3d64be2d3fe56c77f91f6a39da4a5ffc086ecb5db9664e8583
291        // Raw output script: 0x41 0x044bca633a91de10df85a63d0a24cb09783148fe0e16c92e937fc4491580c860757148effa0595a955f44078b48ba67fa198782e8bb68115da0daa8fde5301f7f9 OP_CHECKSIG
292        //                    OP_PUSHDATA0(65 bytes) 0x04bdca... OP_CHECKSIG
293        let bytes = [
294            0x41 as u8, // Push next 65 bytes
295            0x04, 0x4b, 0xca, 0x63, 0x3a, 0x91, 0xde, 0x10, 0xdf, 0x85, 0xa6, 0x3d, 0x0a, 0x24,
296            0xcb, 0x09, 0x78, 0x31, 0x48, 0xfe, 0x0e, 0x16, 0xc9, 0x2e, 0x93, 0x7f, 0xc4, 0x49,
297            0x15, 0x80, 0xc8, 0x60, 0x75, 0x71, 0x48, 0xef, 0xfa, 0x05, 0x95, 0xa9, 0x55, 0xf4,
298            0x40, 0x78, 0xb4, 0x8b, 0xa6, 0x7f, 0xa1, 0x98, 0x78, 0x2e, 0x8b, 0xb6, 0x81, 0x15,
299            0xda, 0x0d, 0xaa, 0x8f, 0xde, 0x53, 0x01, 0xf7, 0xf9, 0xac,
300        ]; // OP_CHECKSIG
301        let result = evaluate_script(
302            &Script::from_hex(&bytes.to_hex()).unwrap(),
303            Network::Bitcoin,
304        );
305        assert_eq!(
306            result.addresses.get(0).unwrap().to_string(),
307            String::from("1LEWwJkDj8xriE87ALzQYcHjTmD8aqDj1f")
308        );
309        assert_eq!(result.pattern, ScriptType::Pay2PublicKey);
310    }
311
312    #[test]
313    fn test_bitcoin_script_p2ms() {
314        // 2-of-3 Multi sig output
315        // OP_2 33 0x022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da
316        // 33 0x03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9
317        // 33 0x021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18 OP_3 OP_CHECKMULTISIG
318        let bytes = [
319            0x52 as u8, 0x21, 0x02, 0x2d, 0xf8, 0x75, 0x04, 0x80, 0xad, 0x5b, 0x26, 0x95, 0x0b,
320            0x25, 0xc7, 0xba, 0x79, 0xd3, 0xe3, 0x7d, 0x75, 0xf6, 0x40, 0xf8, 0xe5, 0xd9, 0xbc,
321            0xd5, 0xb1, 0x50, 0xa0, 0xf8, 0x50, 0x14, 0xda, 0x21, 0x03, 0xe3, 0x81, 0x8b, 0x65,
322            0xbc, 0xc7, 0x3a, 0x7d, 0x64, 0x06, 0x41, 0x06, 0xa8, 0x59, 0xcc, 0x1a, 0x5a, 0x72,
323            0x8c, 0x43, 0x45, 0xff, 0x0b, 0x64, 0x12, 0x09, 0xfb, 0xa0, 0xd9, 0x0d, 0xe6, 0xe9,
324            0x21, 0x02, 0x1f, 0x2f, 0x6e, 0x1e, 0x50, 0xcb, 0x6a, 0x95, 0x39, 0x35, 0xc3, 0x60,
325            0x12, 0x84, 0x92, 0x5d, 0xec, 0xd3, 0xfd, 0x21, 0xbc, 0x44, 0x57, 0x12, 0x57, 0x68,
326            0x73, 0xfb, 0x8c, 0x6e, 0xbc, 0x18, 0x53, 0xae,
327        ];
328
329        let result = evaluate_script(
330            &Script::from_hex(&bytes.to_hex()).unwrap(),
331            Network::Bitcoin,
332        );
333        assert_eq!(result.pattern, ScriptType::Pay2MultiSig);
334    }
335
336    #[test]
337    fn test_bitcoin_script_p2sh() {
338        // Raw output script: a914e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a
339        //                    OP_HASH160 20 0xe9c3dd0c07aac76179ebc76a6c78d4d67c6c160a OP_EQUAL
340        let bytes = [
341            0xa9 as u8, 0x14, // OP_HASH160, OP_PUSHDATA0(20 bytes)
342            0xe9, 0xc3, 0xdd, 0x0c, 0x07, 0xaa, 0xc7, 0x61, 0x79, 0xeb, 0xc7, 0x6a, 0x6c, 0x78,
343            0xd4, 0xd6, 0x7c, 0x6c, 0x16, 0x0a, 0x87,
344        ]; // OP_EQUAL
345        let result = evaluate_script(
346            &Script::from_hex(&bytes.to_hex()).unwrap(),
347            Network::Bitcoin,
348        );
349        assert_eq!(
350            result.addresses.get(0).unwrap().to_string(),
351            String::from("3P14159f73E4gFr7JterCCQh9QjiTjiZrG")
352        );
353        assert_eq!(result.pattern, ScriptType::Pay2ScriptHash);
354    }
355
356    #[test]
357    fn test_bitcoin_script_non_standard() {
358        // Raw output script: 736372697074
359        //                    OP_IFDUP OP_IF OP_2SWAP OP_VERIFY OP_2OVER OP_DEPTH
360        let bytes = [0x73 as u8, 0x63, 0x72, 0x69, 0x70, 0x74];
361        let result = evaluate_script(
362            &Script::from_hex(&bytes.to_hex()).unwrap(),
363            Network::Bitcoin,
364        );
365        assert_eq!(result.addresses.get(0), None);
366        assert_eq!(result.pattern, ScriptType::NotRecognised);
367    }
368
369    #[test]
370    fn test_bitcoin_bogus_script() {
371        let bytes = [0x4c as u8, 0xFF, 0x00];
372        let result = evaluate_script(
373            &Script::from_hex(&bytes.to_hex()).unwrap(),
374            Network::Bitcoin,
375        );
376        assert_eq!(result.addresses.get(0), None);
377        assert_eq!(result.pattern, ScriptType::NotRecognised);
378    }
379}