rawtx-rs 0.1.8

Makes information about Bitcoin transactions accessible.
Documentation
//! Information about Bitcoin transaction outputs.

use std::fmt;

use bitcoin::{blockdata::opcodes::all as opcodes, script, Amount, TxOut};

use crate::script::Multisig;

#[derive(PartialEq, Eq, Debug, Clone)]
pub struct OutputInfo {
    pub out_type: OutputType,
    pub value: Amount,
}

impl OutputInfo {
    pub fn new(output: &TxOut) -> OutputInfo {
        OutputInfo {
            out_type: output.get_type(),
            value: Amount::from_sat(output.value.to_sat()),
        }
    }

    /// Returns true if the output is an OP_RETURN output (of any [OpReturnFlavor]).
    pub fn is_opreturn(&self) -> bool {
        matches!(self.out_type, OutputType::OpReturn(_))
    }
}

#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum OutputType {
    P2pk,
    P2pkh,
    P2wpkhV0,
    P2ms,
    P2sh,
    P2wshV0,
    OpReturn(OpReturnFlavor),
    P2tr,
    Unknown,
}

#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
pub enum OpReturnFlavor {
    Unspecified,
    WitnessCommitment,
    Omni,
    /// Stacks version 2 blockcommit. OP_RETURN start with `X2[`.
    /// https://forum.stacks.org/t/op-return-outputs/12000
    StacksBlockCommit,
    Len1Byte,
    Len20Byte,
    Len80Byte,
}

impl fmt::Display for OpReturnFlavor {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OpReturnFlavor::Unspecified => write!(f, "OP_RETURN"),
            OpReturnFlavor::WitnessCommitment => write!(f, "Witness Commitment"),
            OpReturnFlavor::Omni => write!(f, "OP_RETURN (OmniLayer)"),
            OpReturnFlavor::StacksBlockCommit => write!(f, "OP_RETURN (Stacks v2 blockcommit)"),
            OpReturnFlavor::Len1Byte => write!(f, "OP_RETURN (0 byte)"),
            OpReturnFlavor::Len20Byte => write!(f, "OP_RETURN (20 byte)"),
            OpReturnFlavor::Len80Byte => write!(f, "OP_RETURN (80 byte)"),
        }
    }
}

impl fmt::Display for OutputType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OutputType::P2pk => write!(f, "P2PK"),
            OutputType::P2pkh => write!(f, "P2PKH"),
            OutputType::P2wpkhV0 => write!(f, "P2WPKH v0"),
            OutputType::P2ms => write!(f, "P2MS"),
            OutputType::P2sh => write!(f, "P2SH"),
            OutputType::P2wshV0 => write!(f, "P2WSH v0"),
            OutputType::OpReturn(flavor) => write!(f, "{}", flavor),
            OutputType::P2tr => write!(f, "P2TR"),
            OutputType::Unknown => write!(f, "UNKNOWN"),
        }
    }
}

pub trait OutputTypeDetection {
    fn get_type(&self) -> OutputType;

    fn is_p2ms(&self) -> bool;
    fn is_p2tr(&self) -> bool;

    // OP_RETURN flavor detection
    fn is_witness_commitment(&self) -> bool;
    fn is_opreturn_omni(&self) -> bool;
    fn is_opreturn_stacks_blockcommit(&self) -> bool;
    fn is_opreturn_with_len(&self, length: usize) -> bool;
}

impl OutputTypeDetection for TxOut {
    fn get_type(&self) -> OutputType {
        if self.script_pubkey.is_p2pkh() {
            OutputType::P2pkh
        } else if self.script_pubkey.is_p2sh() {
            OutputType::P2sh
        } else if self.script_pubkey.is_p2wpkh() {
            OutputType::P2wpkhV0
        } else if self.script_pubkey.is_p2wsh() {
            OutputType::P2wshV0
        } else if self.is_p2tr() {
            OutputType::P2tr
        } else if self.script_pubkey.is_op_return() {
            if self.is_witness_commitment() {
                return OutputType::OpReturn(OpReturnFlavor::WitnessCommitment);
            } else if self.is_opreturn_omni() {
                return OutputType::OpReturn(OpReturnFlavor::Omni);
            } else if self.is_opreturn_stacks_blockcommit() {
                return OutputType::OpReturn(OpReturnFlavor::StacksBlockCommit);
            } else if self.is_opreturn_with_len(1) {
                return OutputType::OpReturn(OpReturnFlavor::Len1Byte);
            } else if self.is_opreturn_with_len(20) {
                return OutputType::OpReturn(OpReturnFlavor::Len20Byte);
            } else if self.is_opreturn_with_len(80) {
                return OutputType::OpReturn(OpReturnFlavor::Len80Byte);
            }
            OutputType::OpReturn(OpReturnFlavor::Unspecified)
        } else if self.script_pubkey.is_p2pk() {
            OutputType::P2pk
        } else if self.is_p2ms() {
            OutputType::P2ms
        } else {
            OutputType::Unknown
        }
    }

    /// Checks if an output pays to a P2MS script.
    ///
    /// A P2MS output has a standard OP_CHECKMULTSIG template as usually seen in
    /// e.g. P2SH redeemscripts as script_pubkey. N and M (n-of-m) can't be
    /// bigger than 3 and m must be bigger than or equal to n;
    /// `script_pubkey: [ <OP_PUSHNUM_N>   M * <pubkey>   <OP_PUSHNUM_M> <OP_CHECKMULTISIG> ]`
    fn is_p2ms(&self) -> bool {
        if let Ok(Some(n_of_m)) = self.script_pubkey.get_opcheckmultisig_n_m() {
            let n = n_of_m.0;
            let m = n_of_m.1;
            if n <= 3 && m <= 3 && m >= n {
                return true;
            }
        }
        false
    }

    /// Checks if an output pays to a P2TR script.
    ///
    /// A P2TR output pushes the witness version 1 followed by a 32-byte schnorr-pubkey
    /// `script_pubkey: [ OP_PUSHNUM_1  <32-byte pubkey> ]`
    fn is_p2tr(&self) -> bool {
        let script_pubkey_bytes = self.script_pubkey.as_bytes();
        if script_pubkey_bytes.len() == 34
            && script_pubkey_bytes[0] == opcodes::OP_PUSHNUM_1.to_u8()
            && script_pubkey_bytes[1] == opcodes::OP_PUSHBYTES_32.to_u8()
        {
            return true;
        }
        false
    }

    /// Checks if an output is a OP_RETURN output meeting the requirements for an wittness commitment
    /// as found in Coinbase transactions.
    ///
    /// A witness commitment is atleast 38 bytes long and starts with `6a24aa21a9ed`. More details
    /// can be found in [BIP-141](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#commitment-structure).
    fn is_witness_commitment(&self) -> bool {
        let script_pubkey_bytes = self.script_pubkey.as_bytes();
        if script_pubkey_bytes.len() >= 38
            && script_pubkey_bytes[0] == 0x6A
            && script_pubkey_bytes[1] == 0x24
            && script_pubkey_bytes[2] == 0xAA
            && script_pubkey_bytes[3] == 0x21
            && script_pubkey_bytes[4] == 0xA9
            && script_pubkey_bytes[5] == 0xED
        {
            return true;
        }
        false
    }

    /// Checks if an output is a OP_RETURN output meeting the requirements for a OmniLayer transaction.
    ///
    /// The data in OmniLayer transactions starts with the String 'omni' which is 6f 6d 6e 69 in hex.
    fn is_opreturn_omni(&self) -> bool {
        let script_pubkey_bytes = self.script_pubkey.as_bytes();
        if script_pubkey_bytes.len() > 6 && script_pubkey_bytes[0] == 0x6A &&
                // -- leaving this out as its not clear if all omni op_returns have the same length
                // script_pubkey_bytes[1] == 0x14 && 
                script_pubkey_bytes[2] == 0x6f &&
                script_pubkey_bytes[3] == 0x6d &&
                script_pubkey_bytes[4] == 0x6e &&
                script_pubkey_bytes[5] == 0x69
        {
            return true;
        }
        false
    }

    /// Checks if an output is a OP_RETURN output meeting the requirements
    /// for a Stacks blockcommit.
    ///
    /// The script_pubkey of a Stacks OP_RETURN block_commit pushes 80 bytes
    /// with 'OP_PUSHDATA1 80'. These 80 bytes start with the string 'X2'
    /// which is 0x58 0x32 in hex followed a '[' (0x5b).
    /// https://forum.stacks.org/t/op-return-outputs/12000
    fn is_opreturn_stacks_blockcommit(&self) -> bool {
        let script_pubkey_bytes = self.script_pubkey.as_bytes();
        if script_pubkey_bytes.len() == 83
            && script_pubkey_bytes[0] == 0x6A
            && script_pubkey_bytes[1] == 0x4C
            && script_pubkey_bytes[2] == 0x50
            && script_pubkey_bytes[3] == 0x58
            && script_pubkey_bytes[4] == 0x32
            && script_pubkey_bytes[5] == 0x5b
        {
            return true;
        }
        false
    }

    /// Compares the data length of an OP_RETURN output with the given `data_length`. Returns
    /// true if equal.
    ///
    /// This assumes OP_RETURN use the minimal data push. That means for data shorter than
    /// or equal to (<=) 75 bytes a OP_PUSHBYTES_X is used. For longer data a OP_PUSHDATA1
    /// is used.
    fn is_opreturn_with_len(&self, data_length: usize) -> bool {
        const MIN_OPRETURN_LEN: usize = 1 + 1; // OP_RETURN OP_0
        const MAX_OPRETURN_LEN: usize = 1 + 1 + 1 + 80; // OP_RETURN OP_PUSHDATA1 data-length [80 btyes]
        const MAX_OPPUSHBYTES_LEN: usize = 1 + 1 + 75; // OP_RETURN OP_PUSHBYTES_75 [75 bytes]

        if self.script_pubkey.len() < MIN_OPRETURN_LEN
            || self.script_pubkey.len() > MAX_OPRETURN_LEN
        {
            return false;
        }

        if !self.script_pubkey.as_bytes()[0] == 0x6A {
            return false;
        }

        if self.script_pubkey.len() <= MAX_OPPUSHBYTES_LEN {
            return self.script_pubkey.len() - 1 - 1 == data_length;
        }

        if self.script_pubkey.len() > MAX_OPPUSHBYTES_LEN {
            return self.script_pubkey.len() - 1 - 1 - 1 == data_length;
        }

        false
    }
}

pub trait OutputSigops {
    fn sigops(&self) -> Result<usize, script::Error>;
}

impl OutputSigops for TxOut {
    fn sigops(&self) -> Result<usize, script::Error> {
        const SIGOPS_SCALE_FACTOR: usize = 4;

        // in P2TR scripts, no sigops are counted
        if self.is_p2tr() {
            return Ok(0);
        }

        // for example, for P2MS script_pubkeys (OP_CHECKMUTLISIG)
        return Ok(SIGOPS_SCALE_FACTOR * self.script_pubkey.count_sigops_legacy());
    }
}

#[cfg(test)]
mod tests {
    use super::{OpReturnFlavor, OutputType, OutputTypeDetection};
    use bitcoin::Transaction;

    #[test]
    fn output_type_detection_p2ms() {
        // mainnet ac1d9ed701af32ea52fabd0834acfb1ba4e3584cf0553551f1b61b3d7fb05ee7
        let raw_tx = hex::decode("0100000001ffc0d6d6b592cd2b4160300a278ea5e250b5055b5536dcfb2da5dcc46022765a00000000694630430220575ddd235a989befbf98f43b008666e56af07be89e47e09d18690c75846fb587021f00830605aa09febc51132001e0dbcad860e54d4657b55aaf961b527a935b8a01210281feb90c058c3436f8bc361930ae99fcfb530a699cdad141d7244bfcad521a1fffffffff03204e0000000000002551210281feb90c058c3436f8bc361930ae99fcfb530a699cdad141d7244bfcad521a1f51ae204e0000000000001976a914a988f8039a203cf86136e0d32b9d77eafa5a6bef88ac46f4d501000000001976a914161d7a3d0ee15c793ab300433192f949d8f3566588ac00000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out0 = &tx.output[0];
        assert!(out0.is_p2ms());
        assert_eq!(out0.get_type(), OutputType::P2ms);
    }
    
    #[test]
    fn output_type_detection_p2ms2() {
        // mainnet d5a02fd4d7e3cf5ca02d2a4c02c8124ba00907eb85801dddfe984428714e3946
        let raw_tx = hex::decode("010000000150db0324e3733b7d4915a42acf51d4cd95629fb5a659da68d01292e3152abf7d010000006b4830450221008c24014a99a87736aa47a773d738cbbcd60dadfbb2aa294d4f00cda1e4dae66f022076fa9be5d50eecd2e8e1dbe364b158f2d5df049cbcd8cc759970dd23fab41423012102dc6546ba58b9bc26365357a428516d48c9bbc230dd6fc72912654aaad460ef19ffffffff02781e00000000000069512102d7f69a1fc373a72468ae84634d9949fdeab4d1c903c6f23a3465f79c889342a421028836687b0c942c94801ce11b2601cbb1e900e6544ef28369e69977195794d47b2102dc6546ba58b9bc26365357a428516d48c9bbc230dd6fc72912654aaad460ef1953ae3c660d00000000001976a914e4e9d188d9806fef75904225f370009aa4103a9d88ac00000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out0 = &tx.output[0];
        assert!(out0.is_p2ms());
        assert_eq!(out0.get_type(), OutputType::P2ms);
    }


    #[test]
    fn output_type_detection_p2tr() {
        // signet 9f3d438ab92e86bd86c64749416df8d3a48bcef97b7c32ccefc2ec4f02caac74
        let raw_tx = hex::decode("020000000001029dac93ef467e6035bf641f4076b2a8ac6a4368e93d6c7dc8dcfb38b9bed7da840100000000feffffffbe415b1058e5294f30ccc12332d00636aa8874448141a0446737a1ffc7e6f5060100000000feffffff0410270000000000002251207a61c588fd357d8ed58f624fa7f97a651d1ac00b53b055e9b852507dd319a3d41027000000000000225120acd385f4c428f2ce97644de474a579a77435f40b6161d1c1875f48f2626fccde1e0e1e00000000001600147f611a8cfa64617c05c1b44341b4e469631371c3102700000000000022512070271d98a521d0e4102ebdbc40f3e553666fb5b85c8c3d2709138568c6c90b230247304402202945170a29517bf8773f6a741e587d87b3f4ec6e7348fae8443d45bc5a30f82402200207fcdb3369e55060725bdc2343236271e2dddb62a3077577a85e6f79d22404012103f682085f03c8a27288258933370b4cef8badb4c8a0e8bbfa31d78a450dffd543024730440220711d103aaed2122a8ddef8fd5523ccc7e3748382804dddccdf46e4755c2d1e9f022060e0564f3bf307d5c2128a4bcfd521c33a2bf1c3590cfc0d4fa7c8e02af26ab4012103f682085f03c8a27288258933370b4cef8badb4c8a0e8bbfa31d78a450dffd54300000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out0 = &tx.output[0];
        let out1 = &tx.output[1];
        let out3 = &tx.output[3];
        assert!(out0.is_p2tr());
        assert!(out1.is_p2tr());
        assert!(out3.is_p2tr());
        assert_eq!(out0.get_type(), OutputType::P2tr);
        assert_eq!(out1.get_type(), OutputType::P2tr);
        assert_eq!(out3.get_type(), OutputType::P2tr);
    }

    #[test]
    fn output_type_detection_witness_commitment() {
        // mainnet 2a352a3473385dc9f7b79967aba7aeaafa5f7994d5031ac5b43d168b7566c092
        // coinbase of 00000000000000000009a77c962fabb1b12c54dc1e978080df0155381f97fb5f (674485)
        let raw_tx = hex::decode("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5403b54a0a41d8134fa906ec3741d8134fa83cdae32f3154486173682ffabe6d6d022644aec8e65c41869919439905bd5a6546045825016e8ab843121b089ee79f8000000000000000bf00d52a508f000000000000ffffffff02114cf82c000000001976a9142220867b1e79c403fafe339a809a65ed01cb697988ac0000000000000000266a24aa21a9ed0a8154218fc45bc35f274fafd2490849f8b88f75b3cd63b95096b2a861018f300120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out1 = &tx.output[1];
        assert!(out1.is_witness_commitment());
        assert_eq!(
            out1.get_type(),
            OutputType::OpReturn(OpReturnFlavor::WitnessCommitment)
        );
    }

    #[test]
    fn output_type_detection_opreturn_omni() {
        // mainnet 60d37517d7f6140a560c1aa3961f8c00ac663c73620825e889fb9e19b62d3ad7
        let raw_tx = hex::decode("0100000001305fe5e034c625b571638cbce7837970ca1e84830e794c77de6582ea419bcfb5000000006a47304402206d3f22eff2a26e7f6e2c6e1ccfafa0ae174b5c265353b19d8ee3510316de40ed02201a49d55eaad2f78e7a0358e6ec0f9230ace39451c883845ad107af8822e1ccef0121030651e1d15ae9a284ffd712885529d3344db3700be756e6c22c56a6c1b57d359dffffffff03f64e0600000000001976a914b64513c1f1b889a556463243cca9c26ee626b9a088ac22020000000000001976a914c958135faa72449c106564acba252cfbc3a35ca688ac0000000000000000166a146f6d6e69000000000000001f000002115728ef0000000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out2 = &tx.output[2];
        assert!(out2.is_opreturn_omni());
        assert_eq!(out2.get_type(), OutputType::OpReturn(OpReturnFlavor::Omni));
    }

    #[test]
    fn output_type_detection_opreturn_stacks_blockcommmit() {
        // mainnet 04496abaffb19abf1390b8fc94a8e487eba640c8adc385cfc951637b963fc86a
        let raw_tx = hex::decode("01000000018d9da5a2bc0789aa38bd9e2c1248a5ddaea8f00e097748ca80695f6333adcc04030000006a47304402200704d5f943227080b0c1b8668a37d819804a5c99a0bdda593b3dc167c32a39d402207d2bbd4384e369d671f0c46a54fc4fbfeb14724ab99ea695d1184d4f1074ad18012103fb3bc5bae4c088ca38a8c68bfe741f3b1cb62a067b69917908089a2082af31aefdffffff040000000000000000536a4c5058325b1352fba61836c82246b240fb64043b3e705f8975aa2062d886e247aaeee76ad26f14f91d22c38c8c2fd41ff85e4b22b0cf97b412c53f9aa0182bd6deb51c9567000a3596004b000a2f80012b03989f0400000000001976a914000000000000000000000000000000000000000088ac989f0400000000001976a914000000000000000000000000000000000000000088ac1132920b000000001976a9142c16c83270b688fa3ac46dc69cc01f6321bce41088ac00000000").unwrap();
        let tx: Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
        let out2 = &tx.output[0];
        assert!(out2.is_opreturn_stacks_blockcommit());
        assert_eq!(
            out2.get_type(),
            OutputType::OpReturn(OpReturnFlavor::StacksBlockCommit)
        );
    }
}