Skip to main content

dpp/identity/
core_script.rs

1use bincode::de::{BorrowDecoder, Decoder};
2use bincode::enc::Encoder;
3use bincode::error::{DecodeError, EncodeError};
4use bincode::{BorrowDecode, Decode, Encode};
5use dashcore::blockdata::opcodes;
6use std::fmt;
7use std::ops::Deref;
8
9use dashcore::{ScriptBuf as DashcoreScript, ScriptBuf};
10use platform_value::string_encoding::{self, Encoding};
11use rand::rngs::StdRng;
12use rand::Rng;
13
14use serde::de::Visitor;
15use serde::{Deserialize, Serialize};
16
17use crate::ProtocolError;
18use bincode::de::read::Reader;
19
20#[derive(Clone, Debug, Eq, PartialEq, Default)]
21pub struct CoreScript(DashcoreScript);
22
23impl CoreScript {
24    pub fn new(script: DashcoreScript) -> Self {
25        CoreScript(script)
26    }
27
28    pub fn to_string(&self, encoding: Encoding) -> String {
29        string_encoding::encode(&self.0.to_bytes(), encoding)
30    }
31
32    pub fn from_string(encoded_value: &str, encoding: Encoding) -> Result<Self, ProtocolError> {
33        let vec = string_encoding::decode(encoded_value, encoding)?;
34
35        Ok(Self(vec.into()))
36    }
37
38    pub fn from_bytes(bytes: Vec<u8>) -> Self {
39        Self(bytes.into())
40    }
41
42    pub fn new_p2pkh(key_hash: [u8; 20]) -> Self {
43        let mut bytes: Vec<u8> = vec![
44            opcodes::all::OP_DUP.to_u8(),
45            opcodes::all::OP_HASH160.to_u8(),
46            opcodes::all::OP_PUSHBYTES_20.to_u8(),
47        ];
48        bytes.extend_from_slice(&key_hash);
49        bytes.push(opcodes::all::OP_EQUALVERIFY.to_u8());
50        bytes.push(opcodes::all::OP_CHECKSIG.to_u8());
51        Self::from_bytes(bytes)
52    }
53
54    pub fn new_p2sh(script_hash: [u8; 20]) -> Self {
55        let mut bytes = vec![
56            opcodes::all::OP_HASH160.to_u8(),
57            opcodes::all::OP_PUSHBYTES_20.to_u8(),
58        ];
59        bytes.extend_from_slice(&script_hash);
60        bytes.push(opcodes::all::OP_EQUAL.to_u8());
61        Self::from_bytes(bytes)
62    }
63
64    pub fn random_p2sh(rng: &mut StdRng) -> Self {
65        Self::new_p2sh(rng.gen())
66    }
67
68    pub fn random_p2pkh(rng: &mut StdRng) -> Self {
69        Self::new_p2pkh(rng.gen())
70    }
71}
72
73impl From<Vec<u8>> for CoreScript {
74    fn from(value: Vec<u8>) -> Self {
75        CoreScript::from_bytes(value)
76    }
77}
78
79impl Deref for CoreScript {
80    type Target = DashcoreScript;
81
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87// Implement the bincode::Encode trait for CoreScript
88impl Encode for CoreScript {
89    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
90        self.0.as_bytes().encode(encoder)
91    }
92}
93
94// Implement the bincode::Decode trait for CoreScript
95impl<C> Decode<C> for CoreScript {
96    fn decode<D: Decoder<Context = C>>(decoder: &mut D) -> Result<Self, DecodeError> {
97        let bytes = Vec::<u8>::decode(decoder)?;
98        // Create a CoreScript instance using the decoded DashCoreScript
99        Ok(CoreScript(ScriptBuf(bytes)))
100    }
101}
102
103impl<'de, C> BorrowDecode<'de, C> for CoreScript {
104    fn borrow_decode<D: BorrowDecoder<'de, Context = C>>(
105        decoder: &mut D,
106    ) -> Result<Self, DecodeError> {
107        // Read the serialized bytes from the decoder into a Vec<u8>
108        let mut bytes = Vec::new();
109        loop {
110            let buf_len = 1024; // Adjust the buffer size as needed
111            let mut buf = vec![0; buf_len];
112
113            match decoder.reader().read(&mut buf) {
114                Ok(()) => {
115                    let read_bytes = buf.iter().position(|&x| x == 0).unwrap_or(buf.len());
116                    bytes.extend_from_slice(&buf[..read_bytes]);
117                    if read_bytes < buf_len {
118                        break;
119                    }
120                }
121                Err(DecodeError::Io { inner, additional })
122                    if inner.kind() == std::io::ErrorKind::UnexpectedEof =>
123                {
124                    if additional > 0 {
125                        return Err(DecodeError::Io { inner, additional });
126                    } else {
127                        break;
128                    }
129                }
130                Err(e) => return Err(e),
131            }
132        }
133
134        // Convert Vec<u8> to Box<[u8]> and create a DashCoreScript instance
135        let dash_core_script = DashcoreScript(bytes);
136
137        // Create a CoreScript instance using the decoded DashCoreScript
138        Ok(CoreScript(dash_core_script))
139    }
140}
141
142impl Serialize for CoreScript {
143    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
144    where
145        S: serde::Serializer,
146    {
147        if serializer.is_human_readable() {
148            serializer.serialize_str(&self.to_string(Encoding::Base64))
149        } else {
150            serializer.serialize_bytes(self.as_bytes())
151        }
152    }
153}
154
155impl<'de> Deserialize<'de> for CoreScript {
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: serde::Deserializer<'de>,
159    {
160        // Both visitors accept strings AND bytes regardless of the deserializer's
161        // human-readable flag — same pattern as `BinaryData` / `Identifier`. This
162        // covers the case where serde-wasm-bindgen emits a `Uint8Array` for the
163        // Object form (which the deserializer reports as human-readable), and the
164        // case where a JSON consumer sends a base64 string through the binary path.
165
166        struct CoreScriptVisitor;
167
168        impl Visitor<'_> for CoreScriptVisitor {
169            type Value = CoreScript;
170
171            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
172                formatter.write_str("a byte array or base64-encoded string")
173            }
174
175            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
176                CoreScript::from_string(v, Encoding::Base64).map_err(|e| {
177                    E::custom(format!(
178                        "expected to be able to deserialize core script from string: {}",
179                        e
180                    ))
181                })
182            }
183
184            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
185                self.visit_str(&v)
186            }
187
188            fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
189                Ok(CoreScript::from_bytes(v.to_vec()))
190            }
191
192            fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
193                Ok(CoreScript::from_bytes(v))
194            }
195        }
196
197        if deserializer.is_human_readable() {
198            deserializer.deserialize_string(CoreScriptVisitor)
199        } else {
200            deserializer.deserialize_bytes(CoreScriptVisitor)
201        }
202    }
203}
204
205impl std::fmt::Display for CoreScript {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        write!(f, "{}", self.to_string(Encoding::Base64))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use dashcore::blockdata::opcodes;
215    use platform_value::string_encoding::Encoding;
216
217    mod construction {
218        use super::*;
219
220        #[test]
221        fn from_bytes_creates_script() {
222            let bytes = vec![1, 2, 3, 4, 5];
223            let script = CoreScript::from_bytes(bytes.clone());
224            assert_eq!(script.as_bytes(), &bytes);
225        }
226
227        #[test]
228        fn new_wraps_dashcore_script() {
229            let dashcore_script = DashcoreScript::from(vec![10, 20, 30]);
230            let script = CoreScript::new(dashcore_script.clone());
231            assert_eq!(script.as_bytes(), dashcore_script.as_bytes());
232        }
233
234        #[test]
235        fn default_is_empty() {
236            let script = CoreScript::default();
237            assert!(script.as_bytes().is_empty());
238        }
239
240        #[test]
241        fn from_vec_u8() {
242            let bytes = vec![0xAA, 0xBB, 0xCC];
243            let script: CoreScript = bytes.clone().into();
244            assert_eq!(script.as_bytes(), &bytes);
245        }
246    }
247
248    mod p2pkh {
249        use super::*;
250
251        #[test]
252        fn new_p2pkh_has_correct_structure() {
253            let key_hash = [0u8; 20];
254            let script = CoreScript::new_p2pkh(key_hash);
255            let bytes = script.as_bytes();
256
257            // P2PKH script: OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
258            assert_eq!(bytes.len(), 25); // 3 + 20 + 2
259            assert_eq!(bytes[0], opcodes::all::OP_DUP.to_u8());
260            assert_eq!(bytes[1], opcodes::all::OP_HASH160.to_u8());
261            assert_eq!(bytes[2], opcodes::all::OP_PUSHBYTES_20.to_u8());
262            assert_eq!(&bytes[3..23], &key_hash);
263            assert_eq!(bytes[23], opcodes::all::OP_EQUALVERIFY.to_u8());
264            assert_eq!(bytes[24], opcodes::all::OP_CHECKSIG.to_u8());
265        }
266
267        #[test]
268        fn new_p2pkh_with_nonzero_hash() {
269            let key_hash: [u8; 20] = [
270                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
271                0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
272            ];
273            let script = CoreScript::new_p2pkh(key_hash);
274            let bytes = script.as_bytes();
275            assert_eq!(&bytes[3..23], &key_hash);
276        }
277
278        #[test]
279        fn two_different_key_hashes_produce_different_scripts() {
280            let hash_a = [0xAA; 20];
281            let hash_b = [0xBB; 20];
282            let script_a = CoreScript::new_p2pkh(hash_a);
283            let script_b = CoreScript::new_p2pkh(hash_b);
284            assert_ne!(script_a, script_b);
285        }
286    }
287
288    mod p2sh {
289        use super::*;
290
291        #[test]
292        fn new_p2sh_has_correct_structure() {
293            let script_hash = [0u8; 20];
294            let script = CoreScript::new_p2sh(script_hash);
295            let bytes = script.as_bytes();
296
297            // P2SH script: OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
298            assert_eq!(bytes.len(), 23); // 2 + 20 + 1
299            assert_eq!(bytes[0], opcodes::all::OP_HASH160.to_u8());
300            assert_eq!(bytes[1], opcodes::all::OP_PUSHBYTES_20.to_u8());
301            assert_eq!(&bytes[2..22], &script_hash);
302            assert_eq!(bytes[22], opcodes::all::OP_EQUAL.to_u8());
303        }
304
305        #[test]
306        fn new_p2sh_with_nonzero_hash() {
307            let script_hash: [u8; 20] = [0xFF; 20];
308            let script = CoreScript::new_p2sh(script_hash);
309            let bytes = script.as_bytes();
310            assert_eq!(&bytes[2..22], &script_hash);
311        }
312
313        #[test]
314        fn p2pkh_and_p2sh_differ_for_same_hash() {
315            let hash = [0x42; 20];
316            let p2pkh = CoreScript::new_p2pkh(hash);
317            let p2sh = CoreScript::new_p2sh(hash);
318            assert_ne!(p2pkh, p2sh);
319            // P2PKH is 25 bytes, P2SH is 23 bytes
320            assert_eq!(p2pkh.as_bytes().len(), 25);
321            assert_eq!(p2sh.as_bytes().len(), 23);
322        }
323    }
324
325    mod string_encoding_round_trip {
326        use super::*;
327
328        #[test]
329        fn base64_round_trip() {
330            let original = CoreScript::new_p2pkh([0xAB; 20]);
331            let encoded = original.to_string(Encoding::Base64);
332            let decoded =
333                CoreScript::from_string(&encoded, Encoding::Base64).expect("should decode base64");
334            assert_eq!(original, decoded);
335        }
336
337        #[test]
338        fn hex_round_trip() {
339            let original = CoreScript::new_p2sh([0xCD; 20]);
340            let encoded = original.to_string(Encoding::Hex);
341            let decoded =
342                CoreScript::from_string(&encoded, Encoding::Hex).expect("should decode hex");
343            assert_eq!(original, decoded);
344        }
345
346        #[test]
347        fn from_string_invalid_base64_fails() {
348            let result = CoreScript::from_string("not-valid-base64!!!", Encoding::Base64);
349            assert!(result.is_err());
350        }
351
352        #[test]
353        fn display_uses_base64() {
354            let script = CoreScript::new_p2pkh([0x00; 20]);
355            let display_str = format!("{}", script);
356            let encoded = script.to_string(Encoding::Base64);
357            assert_eq!(display_str, encoded);
358        }
359    }
360
361    mod from_bytes_round_trip {
362        use super::*;
363
364        #[test]
365        fn bytes_round_trip() {
366            let original_bytes = vec![1, 2, 3, 4, 5, 6, 7, 8];
367            let script = CoreScript::from_bytes(original_bytes.clone());
368            assert_eq!(script.as_bytes(), &original_bytes);
369        }
370
371        #[test]
372        fn empty_bytes() {
373            let script = CoreScript::from_bytes(vec![]);
374            assert!(script.as_bytes().is_empty());
375        }
376    }
377
378    mod deref {
379        use super::*;
380
381        #[test]
382        fn deref_returns_inner_script() {
383            let bytes = vec![1, 2, 3];
384            let script = CoreScript::from_bytes(bytes.clone());
385            // Deref gives us access to DashcoreScript methods
386            let inner: &DashcoreScript = &script;
387            assert_eq!(inner.as_bytes(), &bytes);
388        }
389    }
390
391    mod equality_and_clone {
392        use super::*;
393
394        #[test]
395        fn equal_scripts_are_equal() {
396            let a = CoreScript::new_p2pkh([0x11; 20]);
397            let b = CoreScript::new_p2pkh([0x11; 20]);
398            assert_eq!(a, b);
399        }
400
401        #[test]
402        fn different_scripts_are_not_equal() {
403            let a = CoreScript::new_p2pkh([0x11; 20]);
404            let b = CoreScript::new_p2pkh([0x22; 20]);
405            assert_ne!(a, b);
406        }
407
408        #[test]
409        fn clone_produces_equal_script() {
410            let original = CoreScript::new_p2sh([0x33; 20]);
411            let cloned = original.clone();
412            assert_eq!(original, cloned);
413        }
414    }
415
416    mod random_scripts {
417        use super::*;
418        use rand::SeedableRng;
419
420        #[test]
421        fn random_p2pkh_produces_valid_script() {
422            let mut rng = StdRng::seed_from_u64(42);
423            let script = CoreScript::random_p2pkh(&mut rng);
424            let bytes = script.as_bytes();
425            assert_eq!(bytes.len(), 25);
426            assert_eq!(bytes[0], opcodes::all::OP_DUP.to_u8());
427        }
428
429        #[test]
430        fn random_p2sh_produces_valid_script() {
431            let mut rng = StdRng::seed_from_u64(42);
432            let script = CoreScript::random_p2sh(&mut rng);
433            let bytes = script.as_bytes();
434            assert_eq!(bytes.len(), 23);
435            assert_eq!(bytes[0], opcodes::all::OP_HASH160.to_u8());
436        }
437
438        #[test]
439        fn two_random_scripts_differ() {
440            let mut rng = StdRng::seed_from_u64(42);
441            let a = CoreScript::random_p2pkh(&mut rng);
442            let b = CoreScript::random_p2pkh(&mut rng);
443            assert_ne!(a, b);
444        }
445    }
446}