kaspa_txscript/
script_class.rs

1use crate::{opcodes, MAX_SCRIPT_PUBLIC_KEY_VERSION};
2use borsh::{BorshDeserialize, BorshSerialize};
3use kaspa_addresses::Version;
4use kaspa_consensus_core::tx::{ScriptPublicKey, ScriptPublicKeyVersion};
5use serde::{Deserialize, Serialize};
6use std::{
7    fmt::{Display, Formatter},
8    str::FromStr,
9};
10use thiserror::Error;
11
12#[derive(Error, PartialEq, Eq, Debug, Clone)]
13pub enum Error {
14    #[error("Invalid script class {0}")]
15    InvalidScriptClass(String),
16}
17
18/// Standard classes of script payment in the blockDAG
19#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
20#[borsh(use_discriminant = true)]
21#[repr(u8)]
22pub enum ScriptClass {
23    /// None of the recognized forms
24    NonStandard = 0,
25    /// Pay to pubkey
26    PubKey,
27    /// Pay to pubkey ECDSA
28    PubKeyECDSA,
29    /// Pay to script hash
30    ScriptHash,
31}
32
33const NON_STANDARD: &str = "nonstandard";
34const PUB_KEY: &str = "pubkey";
35const PUB_KEY_ECDSA: &str = "pubkeyecdsa";
36const SCRIPT_HASH: &str = "scripthash";
37
38impl ScriptClass {
39    pub fn from_script(script_public_key: &ScriptPublicKey) -> Self {
40        let script_public_key_ = script_public_key.script();
41        if script_public_key.version() == MAX_SCRIPT_PUBLIC_KEY_VERSION {
42            if Self::is_pay_to_pubkey(script_public_key_) {
43                ScriptClass::PubKey
44            } else if Self::is_pay_to_pubkey_ecdsa(script_public_key_) {
45                Self::PubKeyECDSA
46            } else if Self::is_pay_to_script_hash(script_public_key_) {
47                Self::ScriptHash
48            } else {
49                ScriptClass::NonStandard
50            }
51        } else {
52            ScriptClass::NonStandard
53        }
54    }
55
56    // Returns true if the script passed is a pay-to-pubkey
57    // transaction, false otherwise.
58    #[inline(always)]
59    pub fn is_pay_to_pubkey(script_public_key: &[u8]) -> bool {
60        (script_public_key.len() == 34) && // 2 opcodes number + 32 data
61        (script_public_key[0] == opcodes::codes::OpData32) &&
62        (script_public_key[33] == opcodes::codes::OpCheckSig)
63    }
64
65    // Returns returns true if the script passed is an ECDSA pay-to-pubkey
66    /// transaction, false otherwise.
67    #[inline(always)]
68    pub fn is_pay_to_pubkey_ecdsa(script_public_key: &[u8]) -> bool {
69        (script_public_key.len() == 35) && // 2 opcodes number + 33 data
70        (script_public_key[0] == opcodes::codes::OpData33) &&
71        (script_public_key[34] == opcodes::codes::OpCheckSigECDSA)
72    }
73
74    /// Returns true if the script is in the standard
75    /// pay-to-script-hash (P2SH) format, false otherwise.
76    #[inline(always)]
77    pub fn is_pay_to_script_hash(script_public_key: &[u8]) -> bool {
78        (script_public_key.len() == 35) && // 3 opcodes number + 32 data
79        (script_public_key[0] == opcodes::codes::OpBlake2b) &&
80        (script_public_key[1] == opcodes::codes::OpData32) &&
81        (script_public_key[34] == opcodes::codes::OpEqual)
82    }
83
84    fn as_str(&self) -> &'static str {
85        match self {
86            ScriptClass::NonStandard => NON_STANDARD,
87            ScriptClass::PubKey => PUB_KEY,
88            ScriptClass::PubKeyECDSA => PUB_KEY_ECDSA,
89            ScriptClass::ScriptHash => SCRIPT_HASH,
90        }
91    }
92
93    pub fn version(&self) -> ScriptPublicKeyVersion {
94        match self {
95            ScriptClass::NonStandard => 0,
96            ScriptClass::PubKey => MAX_SCRIPT_PUBLIC_KEY_VERSION,
97            ScriptClass::PubKeyECDSA => MAX_SCRIPT_PUBLIC_KEY_VERSION,
98            ScriptClass::ScriptHash => MAX_SCRIPT_PUBLIC_KEY_VERSION,
99        }
100    }
101}
102
103impl Display for ScriptClass {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        f.write_str(self.as_str())
106    }
107}
108
109impl FromStr for ScriptClass {
110    type Err = Error;
111
112    fn from_str(script_class: &str) -> Result<Self, Self::Err> {
113        match script_class {
114            NON_STANDARD => Ok(ScriptClass::NonStandard),
115            PUB_KEY => Ok(ScriptClass::PubKey),
116            PUB_KEY_ECDSA => Ok(ScriptClass::PubKeyECDSA),
117            SCRIPT_HASH => Ok(ScriptClass::ScriptHash),
118            _ => Err(Error::InvalidScriptClass(script_class.to_string())),
119        }
120    }
121}
122
123impl TryFrom<&str> for ScriptClass {
124    type Error = Error;
125
126    fn try_from(script_class: &str) -> Result<Self, Self::Error> {
127        script_class.parse()
128    }
129}
130
131impl From<Version> for ScriptClass {
132    fn from(value: Version) -> Self {
133        match value {
134            Version::PubKey => ScriptClass::PubKey,
135            Version::PubKeyECDSA => ScriptClass::PubKeyECDSA,
136            Version::ScriptHash => ScriptClass::ScriptHash,
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use kaspa_consensus_core::tx::ScriptVec;
144
145    use super::*;
146
147    #[test]
148    fn test_script_class_from_script() {
149        struct Test {
150            name: &'static str,
151            script: Vec<u8>,
152            version: ScriptPublicKeyVersion,
153            class: ScriptClass,
154        }
155
156        // cspell:disable
157        let tests = vec![
158            Test {
159                name: "valid pubkey script",
160                script: hex::decode("204a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f8151ac").unwrap(),
161                version: 0,
162                class: ScriptClass::PubKey,
163            },
164            Test {
165                name: "valid pubkey ecdsa script",
166                script: hex::decode("21fd4a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f8151ab").unwrap(),
167                version: 0,
168                class: ScriptClass::PubKeyECDSA,
169            },
170            Test {
171                name: "valid scripthash script",
172                script: hex::decode("aa204a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f815187").unwrap(),
173                version: 0,
174                class: ScriptClass::ScriptHash,
175            },
176            Test {
177                name: "non standard script (unexpected version)",
178                script: hex::decode("204a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f8151ac").unwrap(),
179                version: MAX_SCRIPT_PUBLIC_KEY_VERSION + 1,
180                class: ScriptClass::NonStandard,
181            },
182            Test {
183                name: "non standard script (unexpected key len)",
184                script: hex::decode("1f4a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f81ac").unwrap(),
185                version: 0,
186                class: ScriptClass::NonStandard,
187            },
188            Test {
189                name: "non standard script (unexpected final check sig op)",
190                script: hex::decode("204a23f5eef4b2dead811c7efb4f1afbd8df845e804b6c36a4001fc096e13f8151ad").unwrap(),
191                version: 0,
192                class: ScriptClass::NonStandard,
193            },
194        ];
195        // cspell:enable
196
197        for test in tests {
198            let script_public_key = ScriptPublicKey::new(test.version, ScriptVec::from_iter(test.script.iter().copied()));
199            assert_eq!(test.class, ScriptClass::from_script(&script_public_key), "{} wrong script class", test.name);
200        }
201    }
202}