kaspa_consensus_core/tx/
script_public_key.rs

1use alloc::borrow::Cow;
2use borsh::{BorshDeserialize, BorshSerialize};
3use core::fmt::Formatter;
4use js_sys::Object;
5use kaspa_utils::{
6    hex::{FromHex, ToHex},
7    serde_bytes::FromHexVisitor,
8};
9use serde::{
10    de::{Error, Visitor},
11    Deserialize, Deserializer, Serialize, Serializer,
12};
13use smallvec::SmallVec;
14use std::{
15    collections::HashSet,
16    str::{self, FromStr},
17};
18use wasm_bindgen::prelude::*;
19use workflow_wasm::prelude::*;
20
21/// Size of the underlying script vector of a script.
22pub const SCRIPT_VECTOR_SIZE: usize = 36;
23
24/// Used as the underlying type for script public key data, optimized for the common p2pk script size (34).
25pub type ScriptVec = SmallVec<[u8; SCRIPT_VECTOR_SIZE]>;
26
27/// Represents the ScriptPublicKey Version
28pub type ScriptPublicKeyVersion = u16;
29
30/// Alias the `smallvec!` macro to ease maintenance
31pub use smallvec::smallvec as scriptvec;
32use wasm_bindgen::prelude::wasm_bindgen;
33
34//Represents a Set of [`ScriptPublicKey`]s
35pub type ScriptPublicKeys = HashSet<ScriptPublicKey>;
36
37#[wasm_bindgen(typescript_custom_section)]
38const TS_SCRIPT_PUBLIC_KEY: &'static str = r#"
39/**
40 * Interface defines the structure of a Script Public Key.
41 * 
42 * @category Consensus
43 */
44export interface IScriptPublicKey {
45    version : number;
46    script: HexString;
47}
48"#;
49
50/// Represents a Kaspad ScriptPublicKey
51/// @category Consensus
52#[derive(Default, PartialEq, Eq, Clone, Hash, CastFromJs)]
53#[wasm_bindgen(inspectable)]
54pub struct ScriptPublicKey {
55    pub version: ScriptPublicKeyVersion,
56    pub(super) script: ScriptVec, // Kept private to preserve read-only semantics
57}
58
59impl std::fmt::Debug for ScriptPublicKey {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("ScriptPublicKey").field("version", &self.version).field("script", &self.script.to_hex()).finish()
62    }
63}
64
65impl FromHex for ScriptPublicKey {
66    type Error = faster_hex::Error;
67
68    fn from_hex(hex_str: &str) -> Result<Self, Self::Error> {
69        ScriptPublicKey::from_str(hex_str)
70    }
71}
72
73#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)]
74#[serde(rename_all = "camelCase")]
75#[serde(rename = "ScriptPublicKey")]
76struct ScriptPublicKeyInternal<'a> {
77    version: ScriptPublicKeyVersion,
78    script: &'a [u8],
79}
80
81impl Serialize for ScriptPublicKey {
82    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83    where
84        S: Serializer,
85    {
86        if serializer.is_human_readable() {
87            let mut hex = vec![0u8; self.script.len() * 2 + 4];
88            faster_hex::hex_encode(&self.version.to_be_bytes(), &mut hex).map_err(serde::ser::Error::custom)?;
89            faster_hex::hex_encode(&self.script, &mut hex[4..]).map_err(serde::ser::Error::custom)?;
90            serializer.serialize_str(unsafe { str::from_utf8_unchecked(&hex) })
91        } else {
92            ScriptPublicKeyInternal { version: self.version, script: &self.script }.serialize(serializer)
93        }
94    }
95}
96
97impl<'de: 'a, 'a> Deserialize<'de> for ScriptPublicKey {
98    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
99    where
100        D: Deserializer<'de>,
101    {
102        #[derive(Default)]
103        pub struct ScriptPublicKeyVisitor<'de> {
104            from_hex_visitor: FromHexVisitor<'de, ScriptPublicKey>,
105            marker: std::marker::PhantomData<ScriptPublicKey>,
106            lifetime: std::marker::PhantomData<&'de ()>,
107        }
108        impl<'de> Visitor<'de> for ScriptPublicKeyVisitor<'de> {
109            type Value = ScriptPublicKey;
110
111            fn expecting(&self, formatter: &mut Formatter) -> core::fmt::Result {
112                #[cfg(target_arch = "wasm32")]
113                {
114                    write!(formatter, "string-type: string, str; bytes-type: slice of bytes, vec of bytes; map; number-type - pointer")
115                }
116                #[cfg(not(target_arch = "wasm32"))]
117                {
118                    write!(formatter, "string-type: string, str; bytes-type: slice of bytes, vec of bytes; map")
119                }
120            }
121
122            // TODO - review integer conversions (SPK & Address)
123            // This is currently used to allow for deserialization
124            // of JsValues created via wasm-bindgen bindings in the
125            // WASM context. This is not used in the native context
126            // as serialization will never produce objects.
127            // - review multiple integer mappings (are they all needed?)
128            // - consider manual marshaling of RPC data structures
129            // (which is now possible due to the introduction of the kaspa-consensus-wasm crate)
130            #[cfg(target_arch = "wasm32")]
131            fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
132            where
133                E: serde::de::Error,
134            {
135                self.visit_u32(v as u32)
136            }
137            #[cfg(target_arch = "wasm32")]
138            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
139            where
140                E: serde::de::Error,
141            {
142                self.visit_u32(v as u32)
143            }
144
145            #[cfg(target_arch = "wasm32")]
146            fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
147            where
148                E: serde::de::Error,
149            {
150                self.visit_u32(v as u32)
151            }
152            #[cfg(target_arch = "wasm32")]
153            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
154            where
155                E: serde::de::Error,
156            {
157                self.visit_u32(v as u32)
158            }
159            #[cfg(target_arch = "wasm32")]
160            fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
161            where
162                E: serde::de::Error,
163            {
164                use wasm_bindgen::convert::RefFromWasmAbi;
165                let instance_ref = unsafe { Self::Value::ref_from_abi(v) }; // todo add checks for safecast
166                Ok(instance_ref.clone())
167            }
168            #[cfg(target_arch = "wasm32")]
169            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
170            where
171                E: serde::de::Error,
172            {
173                self.visit_u32(v as u32)
174            }
175
176            #[inline]
177            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
178            where
179                E: Error,
180            {
181                self.from_hex_visitor.visit_str(v)
182            }
183
184            #[inline]
185            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
186            where
187                E: Error,
188            {
189                self.from_hex_visitor.visit_borrowed_str(v)
190            }
191
192            #[inline]
193            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
194            where
195                E: Error,
196            {
197                self.from_hex_visitor.visit_string(v)
198            }
199
200            #[inline]
201            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
202            where
203                E: Error,
204            {
205                self.from_hex_visitor.visit_bytes(v)
206            }
207
208            #[inline]
209            fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
210            where
211                E: Error,
212            {
213                self.from_hex_visitor.visit_borrowed_bytes(v)
214            }
215
216            #[inline]
217            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
218            where
219                E: Error,
220            {
221                self.from_hex_visitor.visit_byte_buf(v)
222            }
223
224            #[inline]
225            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
226            where
227                A: serde::de::MapAccess<'de>,
228            {
229                #[derive(Deserialize, Copy, Clone)]
230                #[serde(field_identifier, rename_all = "lowercase")]
231                enum Field {
232                    Version,
233                    Script,
234                }
235
236                #[derive(Debug, Clone, Deserialize)]
237                #[serde(untagged)]
238                pub enum Value<'a> {
239                    U16(u16),
240                    #[serde(borrow)]
241                    String(Cow<'a, String>),
242                }
243                impl From<Value<'_>> for u16 {
244                    fn from(value: Value<'_>) -> Self {
245                        let Value::U16(v) = value else { panic!("unexpected conversion: {value:?}") };
246                        v
247                    }
248                }
249
250                impl TryFrom<Value<'_>> for Vec<u8> {
251                    type Error = faster_hex::Error;
252
253                    fn try_from(value: Value) -> Result<Self, Self::Error> {
254                        match value {
255                            Value::U16(_) => {
256                                panic!("unexpected conversion: {value:?}")
257                            }
258                            Value::String(script) => {
259                                let mut script_bytes = vec![0u8; script.len() / 2];
260                                faster_hex::hex_decode(script.as_bytes(), script_bytes.as_mut_slice())?;
261
262                                Ok(script_bytes)
263                            }
264                        }
265                    }
266                }
267
268                let mut version: Option<u16> = None;
269                let mut script: Option<Vec<u8>> = None;
270
271                while let Some((key, value)) = access.next_entry::<Field, Value>()? {
272                    match key {
273                        Field::Version => {
274                            version = Some(value.into());
275                        }
276                        Field::Script => script = Some(value.try_into().map_err(Error::custom)?),
277                    }
278                    if version.is_some() && script.is_some() {
279                        break;
280                    }
281                }
282                let (version, script) = match (version, script) {
283                    (Some(version), Some(script)) => Ok((version, script)),
284                    (None, _) => Err(serde::de::Error::missing_field("version")),
285                    (_, None) => Err(serde::de::Error::missing_field("script")),
286                }?;
287
288                Ok(ScriptPublicKey::from_vec(version, script))
289            }
290        }
291
292        if deserializer.is_human_readable() {
293            deserializer.deserialize_any(ScriptPublicKeyVisitor::default())
294        } else {
295            ScriptPublicKeyInternal::deserialize(deserializer)
296                .map(|ScriptPublicKeyInternal { script, version }| Self { version, script: SmallVec::from_slice(script) })
297        }
298    }
299}
300
301impl FromStr for ScriptPublicKey {
302    type Err = faster_hex::Error;
303    fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
304        let hex_len = hex_str.len();
305        if hex_len < 4 {
306            return Err(faster_hex::Error::InvalidLength(hex_len));
307        }
308        let mut bytes = vec![0u8; hex_len / 2];
309        faster_hex::hex_decode(hex_str.as_bytes(), bytes.as_mut_slice())?;
310        let version = u16::from_be_bytes(bytes[0..2].try_into().unwrap());
311        Ok(Self { version, script: SmallVec::from_slice(&bytes[2..]) })
312    }
313}
314
315impl ScriptPublicKey {
316    pub fn new(version: ScriptPublicKeyVersion, script: ScriptVec) -> Self {
317        Self { version, script }
318    }
319
320    pub fn from_vec(version: ScriptPublicKeyVersion, script: Vec<u8>) -> Self {
321        Self { version, script: ScriptVec::from_vec(script) }
322    }
323
324    pub fn version(&self) -> ScriptPublicKeyVersion {
325        self.version
326    }
327
328    pub fn script(&self) -> &[u8] {
329        &self.script
330    }
331}
332
333#[wasm_bindgen]
334extern "C" {
335    #[wasm_bindgen(typescript_type = "ScriptPublicKey | HexString")]
336    pub type ScriptPublicKeyT;
337}
338
339#[wasm_bindgen]
340impl ScriptPublicKey {
341    #[wasm_bindgen(constructor)]
342    pub fn constructor(version: u16, script: JsValue) -> Result<ScriptPublicKey, JsError> {
343        let script = script.try_as_vec_u8()?;
344        Ok(ScriptPublicKey::new(version, script.into()))
345    }
346
347    #[wasm_bindgen(getter = script)]
348    pub fn script_as_hex(&self) -> String {
349        self.script.to_hex()
350    }
351}
352
353//
354// Borsh serializers need to be manually implemented for `ScriptPublicKey` since
355// smallvec does not currently support Borsh
356//
357
358impl BorshSerialize for ScriptPublicKey {
359    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
360        borsh::BorshSerialize::serialize(&self.version, writer)?;
361        // Vectors and slices are all serialized internally the same way
362        borsh::BorshSerialize::serialize(&self.script.as_slice(), writer)?;
363        Ok(())
364    }
365}
366
367impl BorshDeserialize for ScriptPublicKey {
368    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
369        // Deserialize into vec first since we have no custom smallvec support
370        Ok(Self::from_vec(borsh::BorshDeserialize::deserialize_reader(reader)?, borsh::BorshDeserialize::deserialize_reader(reader)?))
371    }
372}
373
374type CastError = workflow_wasm::error::Error;
375impl TryCastFromJs for ScriptPublicKey {
376    type Error = workflow_wasm::error::Error;
377    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<Self>, Self::Error>
378    where
379        R: AsRef<JsValue> + 'a,
380    {
381        Self::resolve(value, || {
382            if let Some(hex_str) = value.as_ref().as_string() {
383                Ok(Self::from_str(&hex_str).map_err(CastError::custom)?)
384            } else if let Some(object) = Object::try_from(value.as_ref()) {
385                let version = object.try_get_value("version")?.ok_or(CastError::custom(
386                    "ScriptPublicKey must be a hex string or an object with 'version' and 'script' properties",
387                ))?;
388
389                let version = if let Ok(version) = version.try_as_u16() {
390                    version
391                } else {
392                    return Err(CastError::custom("Invalid version value '{version:?}'"));
393                };
394
395                let script = object.get_vec_u8("script")?;
396
397                Ok(ScriptPublicKey::from_vec(version, script))
398            } else {
399                Err(CastError::custom(format!("Unable to convert ScriptPublicKey from: {:?}", value.as_ref())))
400            }
401        })
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use js_sys::Object;
409    use wasm_bindgen::__rt::IntoJsResult;
410
411    #[test]
412    fn test_spk_serde_json() {
413        let vec = (0..SCRIPT_VECTOR_SIZE as u8).collect::<Vec<_>>();
414        let spk = ScriptPublicKey::from_vec(0xc0de, vec.clone()); // 0xc0de == 49374,
415        let hex: String = serde_json::to_string(&spk).unwrap();
416        assert_eq!("\"c0de000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223\"", hex);
417        let spk = serde_json::from_str::<ScriptPublicKey>(&hex).unwrap();
418        assert_eq!(spk.version, 0xc0de);
419        assert_eq!(spk.script.as_slice(), vec.as_slice());
420        let result = "00".parse::<ScriptPublicKey>();
421        assert!(matches!(result, Err(faster_hex::Error::InvalidLength(2))));
422        let result = "0000".parse::<ScriptPublicKey>();
423        let _empty = ScriptPublicKey { version: 0, script: ScriptVec::new() };
424        assert!(matches!(result, Ok(_empty)));
425    }
426
427    #[test]
428    fn test_spk_borsh() {
429        // Tests for ScriptPublicKey Borsh ser/deser since we manually implemented them
430        let spk = ScriptPublicKey::from_vec(12, vec![32; 20]);
431        let bin = borsh::to_vec(&spk).unwrap();
432        let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap();
433        assert_eq!(spk, spk2);
434
435        let spk = ScriptPublicKey::from_vec(55455, vec![11; 200]);
436        let bin = borsh::to_vec(&spk).unwrap();
437        let spk2: ScriptPublicKey = BorshDeserialize::try_from_slice(&bin).unwrap();
438        assert_eq!(spk, spk2);
439    }
440
441    use wasm_bindgen::convert::IntoWasmAbi;
442    use wasm_bindgen_test::wasm_bindgen_test;
443    use workflow_wasm::serde::{from_value, to_value};
444
445    #[wasm_bindgen_test]
446    pub fn test_wasm_serde_constructor() {
447        let version = 0xc0de;
448        let vec: Vec<u8> = (0..SCRIPT_VECTOR_SIZE as u8).collect();
449        let spk = ScriptPublicKey::from_vec(version, vec.clone());
450        let str = "c0de000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223";
451        let js = to_value(&spk).unwrap();
452        assert_eq!(js.as_string().unwrap(), str);
453        let script_hex = spk.script_as_hex();
454        let script_hex_js: JsValue = JsValue::from_str(&script_hex);
455
456        let spk_js = to_value(&ScriptPublicKey::constructor(version, script_hex_js).map_err(|_| ()).unwrap()).unwrap();
457        assert_eq!(JsValue::from_str(str), spk_js.clone());
458        assert_eq!(spk, from_value(spk_js.clone()).map_err(|_| ()).unwrap());
459        assert_eq!(JsValue::from_str("string"), spk_js.js_typeof());
460    }
461
462    #[wasm_bindgen_test]
463    pub fn test_wasm_serde_js_spk_object() {
464        let version = 0xc0de;
465        let vec: Vec<u8> = (0..SCRIPT_VECTOR_SIZE as u8).collect();
466        let spk = ScriptPublicKey::from_vec(version, vec.clone());
467
468        let script_hex = spk.script_as_hex();
469
470        let version_js: JsValue = version.into();
471        let script_hex_js: JsValue = JsValue::from_str(&script_hex);
472
473        let obj_hex = Object::new();
474        obj_hex.set("version", &version_js).unwrap();
475        obj_hex.set("script", &script_hex_js).unwrap();
476
477        let actual: ScriptPublicKey = from_value(obj_hex.into_js_result().unwrap()).unwrap();
478        assert_eq!(spk, actual);
479    }
480
481    #[wasm_bindgen_test]
482    pub fn test_wasm_serde_spk_object() {
483        let version = 0xc0de;
484        let vec: Vec<u8> = (0..SCRIPT_VECTOR_SIZE as u8).collect();
485        let spk = ScriptPublicKey::from_vec(version, vec.clone());
486
487        let script_hex = spk.script_as_hex();
488        let script_hex_js: JsValue = JsValue::from_str(&script_hex);
489
490        let expected = ScriptPublicKey::constructor(version, script_hex_js).map_err(|_| ()).unwrap();
491        let wasm_js_value: JsValue = expected.clone().into_abi().into();
492
493        let actual = from_value(wasm_js_value).unwrap();
494        assert_eq!(expected, actual);
495    }
496}