use bitcoin::blockdata::opcodes::{all, All};
use bitcoin::blockdata::script::Instruction;
use bitcoin::hashes::{hash160, Hash};
use bitcoin::util::address::Payload;
use bitcoin::{Address, Network, PubkeyHash, PublicKey, Script};
use serde::{Deserialize, Serialize};
use std::fmt;
use Instruction::{Op, PushBytes};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ScriptType {
OpReturn,
Pay2MultiSig,
Pay2PublicKey,
Pay2PublicKeyHash,
Pay2ScriptHash,
Pay2WitnessPublicKeyHash,
Pay2WitnessScriptHash,
WitnessProgram,
Unspendable,
NotRecognised,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ScriptInfo {
pub addresses: Vec<Address>,
pub pattern: ScriptType,
}
pub fn evaluate_script(script: &Script, net: Network) -> ScriptInfo {
let address = Address::from_script(script, net);
if script.is_p2pk() {
ScriptInfo::new(p2pk_to_address(script), ScriptType::Pay2PublicKey)
} else if script.is_p2pkh() {
ScriptInfo::new(address, ScriptType::Pay2PublicKeyHash)
} else if script.is_p2sh() {
ScriptInfo::new(address, ScriptType::Pay2ScriptHash)
} else if script.is_v0_p2wpkh() {
ScriptInfo::new(address, ScriptType::Pay2WitnessPublicKeyHash)
} else if script.is_v0_p2wsh() {
ScriptInfo::new(address, ScriptType::Pay2WitnessScriptHash)
} else if script.is_witness_program() {
ScriptInfo::new(address, ScriptType::WitnessProgram)
} else if script.is_op_return() {
ScriptInfo::new(address, ScriptType::OpReturn)
} else if script.is_provably_unspendable() {
ScriptInfo::new(address, ScriptType::Unspendable)
} else if is_multisig(script) {
ScriptInfo::from_vec(multisig_addresses(script), ScriptType::Pay2MultiSig)
} else {
ScriptInfo::new(address, ScriptType::NotRecognised)
}
}
impl ScriptInfo {
pub(crate) fn new(address: Option<Address>, pattern: ScriptType) -> Self {
if let Some(address) = address {
Self::from_vec(vec![address], pattern)
} else {
Self::from_vec(Vec::new(), pattern)
}
}
pub(crate) fn from_vec(addresses: Vec<Address>, pattern: ScriptType) -> Self {
Self { addresses, pattern }
}
}
fn is_multisig(script: &Script) -> bool {
let mut chunks: Vec<Instruction> = Vec::new();
for i in script.instructions() {
if let Ok(i) = i {
chunks.push(i);
} else {
return false;
}
}
if chunks.len() < 4 {
return false;
}
match chunks.last().unwrap() {
PushBytes(_) => {
return false;
}
Op(op) => {
if !(op.eq(&all::OP_CHECKMULTISIG) || op.eq(&all::OP_CHECKMULTISIGVERIFY)) {
return false;
}
}
}
let second_last_chunk = chunks.get(chunks.len() - 2).unwrap();
if let Some(num_keys) = get_num_keys(second_last_chunk) {
if num_keys < 1 || (num_keys + 3) as usize != chunks.len() {
return false;
}
} else {
return false;
}
for chunk in chunks.iter().skip(1).take(chunks.len() - 3) {
if let Op(_) = chunk {
return false;
}
}
if let Some(num_keys) = get_num_keys(chunks.first().unwrap()) {
if num_keys < 1 {
return false;
}
} else {
return false;
}
true
}
fn multisig_addresses(script: &Script) -> Vec<Address> {
assert!(is_multisig(script));
let ops: Vec<Instruction> = script.instructions().filter_map(|o| o.ok()).collect();
let num_keys = {
if let Some(Op(op)) = ops.get(ops.len() - 2) {
decode_from_op_n(op)
} else {
unreachable!()
}
};
let mut public_keys = Vec::with_capacity(num_keys as usize);
for op in ops.iter().skip(1).take(num_keys as usize) {
if let PushBytes(data) = op {
match PublicKey::from_slice(data) {
Ok(pk) => public_keys.push(Address {
payload: Payload::PubkeyHash(pk.pubkey_hash()),
network: Network::Bitcoin,
}),
Err(_) => return Vec::new(),
}
} else {
unreachable!()
}
}
public_keys
}
#[inline]
fn decode_from_op_n(op: &All) -> i32 {
if op.eq(&all::OP_PUSHBYTES_0) {
0
} else if op.eq(&all::OP_PUSHNUM_NEG1) {
-1
} else {
op.into_u8() as i32 + 1 - all::OP_PUSHNUM_1.into_u8() as i32
}
}
#[inline]
fn get_num_keys(op: &Instruction) -> Option<i32> {
match op {
PushBytes(_) => None,
Op(op) => {
if !(op.eq(&all::OP_PUSHNUM_NEG1)
|| op.eq(&all::OP_PUSHBYTES_0)
|| (op.ge(&all::OP_PUSHNUM_1) && all::OP_PUSHNUM_16.ge(op)))
{
None
} else {
Some(decode_from_op_n(op))
}
}
}
}
#[inline]
fn p2pk_to_address(script: &Script) -> Option<Address> {
assert!(script.is_p2pk());
if let Some(Ok(Instruction::PushBytes(pk))) = script.instructions().next() {
let pkh = hash160::Hash::hash(pk);
Some(Address {
payload: Payload::PubkeyHash(PubkeyHash::from_slice(&pkh).ok()?),
network: Network::Bitcoin,
})
} else {
unreachable!()
}
}
trait Cmp {
fn ge(&self, other: &Self) -> bool;
}
impl Cmp for bitcoin::blockdata::opcodes::All {
fn ge(&self, other: &Self) -> bool {
self.into_u8() >= other.into_u8()
}
}
impl fmt::Display for ScriptType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ScriptType::OpReturn => write!(f, "OpReturn"),
ScriptType::Pay2MultiSig => write!(f, "Pay2MultiSig"),
ScriptType::Pay2PublicKey => write!(f, "Pay2PublicKey"),
ScriptType::Pay2PublicKeyHash => write!(f, "Pay2PublicKeyHash"),
ScriptType::Pay2ScriptHash => write!(f, "Pay2ScriptHash"),
ScriptType::Pay2WitnessPublicKeyHash => write!(f, "Pay2WitnessPublicKeyHash"),
ScriptType::Pay2WitnessScriptHash => write!(f, "Pay2WitnessScriptHash"),
ScriptType::WitnessProgram => write!(f, "WitnessProgram"),
ScriptType::Unspendable => write!(f, "Unspendable"),
ScriptType::NotRecognised => write!(f, "NotRecognised"),
}
}
}
#[cfg(test)]
mod tests {
use super::{evaluate_script, ScriptType};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::{Network, Script};
#[test]
fn test_bitcoin_script_p2pkh() {
let bytes = [
0x76 as u8, 0xa9, 0x14, 0x12, 0xab, 0x8d, 0xc5, 0x88, 0xca, 0x9d, 0x57, 0x87, 0xdd,
0xe7, 0xeb, 0x29, 0x56, 0x9d, 0xa6, 0x3c, 0x3a, 0x23, 0x8c, 0x88, 0xac,
];
let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(
result.addresses.get(0).unwrap().to_string(),
String::from("12higDjoCCNXSA95xZMWUdPvXNmkAduhWv")
);
assert_eq!(result.pattern, ScriptType::Pay2PublicKeyHash);
}
#[test]
fn test_bitcoin_script_p2pk() {
let bytes = [
0x41 as u8, 0x04, 0x4b, 0xca, 0x63, 0x3a, 0x91, 0xde, 0x10, 0xdf, 0x85, 0xa6, 0x3d, 0x0a, 0x24,
0xcb, 0x09, 0x78, 0x31, 0x48, 0xfe, 0x0e, 0x16, 0xc9, 0x2e, 0x93, 0x7f, 0xc4, 0x49,
0x15, 0x80, 0xc8, 0x60, 0x75, 0x71, 0x48, 0xef, 0xfa, 0x05, 0x95, 0xa9, 0x55, 0xf4,
0x40, 0x78, 0xb4, 0x8b, 0xa6, 0x7f, 0xa1, 0x98, 0x78, 0x2e, 0x8b, 0xb6, 0x81, 0x15,
0xda, 0x0d, 0xaa, 0x8f, 0xde, 0x53, 0x01, 0xf7, 0xf9, 0xac,
]; let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(
result.addresses.get(0).unwrap().to_string(),
String::from("1LEWwJkDj8xriE87ALzQYcHjTmD8aqDj1f")
);
assert_eq!(result.pattern, ScriptType::Pay2PublicKey);
}
#[test]
fn test_bitcoin_script_p2ms() {
let bytes = [
0x52 as u8, 0x21, 0x02, 0x2d, 0xf8, 0x75, 0x04, 0x80, 0xad, 0x5b, 0x26, 0x95, 0x0b,
0x25, 0xc7, 0xba, 0x79, 0xd3, 0xe3, 0x7d, 0x75, 0xf6, 0x40, 0xf8, 0xe5, 0xd9, 0xbc,
0xd5, 0xb1, 0x50, 0xa0, 0xf8, 0x50, 0x14, 0xda, 0x21, 0x03, 0xe3, 0x81, 0x8b, 0x65,
0xbc, 0xc7, 0x3a, 0x7d, 0x64, 0x06, 0x41, 0x06, 0xa8, 0x59, 0xcc, 0x1a, 0x5a, 0x72,
0x8c, 0x43, 0x45, 0xff, 0x0b, 0x64, 0x12, 0x09, 0xfb, 0xa0, 0xd9, 0x0d, 0xe6, 0xe9,
0x21, 0x02, 0x1f, 0x2f, 0x6e, 0x1e, 0x50, 0xcb, 0x6a, 0x95, 0x39, 0x35, 0xc3, 0x60,
0x12, 0x84, 0x92, 0x5d, 0xec, 0xd3, 0xfd, 0x21, 0xbc, 0x44, 0x57, 0x12, 0x57, 0x68,
0x73, 0xfb, 0x8c, 0x6e, 0xbc, 0x18, 0x53, 0xae,
];
let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(result.pattern, ScriptType::Pay2MultiSig);
}
#[test]
fn test_bitcoin_script_p2sh() {
let bytes = [
0xa9 as u8, 0x14, 0xe9, 0xc3, 0xdd, 0x0c, 0x07, 0xaa, 0xc7, 0x61, 0x79, 0xeb, 0xc7, 0x6a, 0x6c, 0x78,
0xd4, 0xd6, 0x7c, 0x6c, 0x16, 0x0a, 0x87,
]; let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(
result.addresses.get(0).unwrap().to_string(),
String::from("3P14159f73E4gFr7JterCCQh9QjiTjiZrG")
);
assert_eq!(result.pattern, ScriptType::Pay2ScriptHash);
}
#[test]
fn test_bitcoin_script_non_standard() {
let bytes = [0x73 as u8, 0x63, 0x72, 0x69, 0x70, 0x74];
let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(result.addresses.get(0), None);
assert_eq!(result.pattern, ScriptType::NotRecognised);
}
#[test]
fn test_bitcoin_bogus_script() {
let bytes = [0x4c as u8, 0xFF, 0x00];
let result = evaluate_script(
&Script::from_hex(&bytes.to_hex()).unwrap(),
Network::Bitcoin,
);
assert_eq!(result.addresses.get(0), None);
assert_eq!(result.pattern, ScriptType::NotRecognised);
}
}