Skip to main content

dpp/identity/
core_script.rs

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