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        if deserializer.is_human_readable() {
161            let data: String = Deserialize::deserialize(deserializer)?;
162
163            Self::from_string(&data, Encoding::Base64).map_err(|e| {
164                serde::de::Error::custom(format!(
165                    "expected to be able to deserialize core script from string: {}",
166                    e
167                ))
168            })
169        } else {
170            struct BytesVisitor;
171
172            impl Visitor<'_> for BytesVisitor {
173                type Value = CoreScript;
174
175                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
176                    formatter.write_str("a byte array")
177                }
178
179                fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
180                where
181                    E: serde::de::Error,
182                {
183                    Ok(CoreScript::from_bytes(v.to_vec()))
184                }
185            }
186
187            deserializer.deserialize_bytes(BytesVisitor)
188        }
189    }
190}
191
192impl std::fmt::Display for CoreScript {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        write!(f, "{}", self.to_string(Encoding::Base64))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use dashcore::blockdata::opcodes;
202    use platform_value::string_encoding::Encoding;
203
204    mod construction {
205        use super::*;
206
207        #[test]
208        fn from_bytes_creates_script() {
209            let bytes = vec![1, 2, 3, 4, 5];
210            let script = CoreScript::from_bytes(bytes.clone());
211            assert_eq!(script.as_bytes(), &bytes);
212        }
213
214        #[test]
215        fn new_wraps_dashcore_script() {
216            let dashcore_script = DashcoreScript::from(vec![10, 20, 30]);
217            let script = CoreScript::new(dashcore_script.clone());
218            assert_eq!(script.as_bytes(), dashcore_script.as_bytes());
219        }
220
221        #[test]
222        fn default_is_empty() {
223            let script = CoreScript::default();
224            assert!(script.as_bytes().is_empty());
225        }
226
227        #[test]
228        fn from_vec_u8() {
229            let bytes = vec![0xAA, 0xBB, 0xCC];
230            let script: CoreScript = bytes.clone().into();
231            assert_eq!(script.as_bytes(), &bytes);
232        }
233    }
234
235    mod p2pkh {
236        use super::*;
237
238        #[test]
239        fn new_p2pkh_has_correct_structure() {
240            let key_hash = [0u8; 20];
241            let script = CoreScript::new_p2pkh(key_hash);
242            let bytes = script.as_bytes();
243
244            // P2PKH script: OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
245            assert_eq!(bytes.len(), 25); // 3 + 20 + 2
246            assert_eq!(bytes[0], opcodes::all::OP_DUP.to_u8());
247            assert_eq!(bytes[1], opcodes::all::OP_HASH160.to_u8());
248            assert_eq!(bytes[2], opcodes::all::OP_PUSHBYTES_20.to_u8());
249            assert_eq!(&bytes[3..23], &key_hash);
250            assert_eq!(bytes[23], opcodes::all::OP_EQUALVERIFY.to_u8());
251            assert_eq!(bytes[24], opcodes::all::OP_CHECKSIG.to_u8());
252        }
253
254        #[test]
255        fn new_p2pkh_with_nonzero_hash() {
256            let key_hash: [u8; 20] = [
257                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
258                0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
259            ];
260            let script = CoreScript::new_p2pkh(key_hash);
261            let bytes = script.as_bytes();
262            assert_eq!(&bytes[3..23], &key_hash);
263        }
264
265        #[test]
266        fn two_different_key_hashes_produce_different_scripts() {
267            let hash_a = [0xAA; 20];
268            let hash_b = [0xBB; 20];
269            let script_a = CoreScript::new_p2pkh(hash_a);
270            let script_b = CoreScript::new_p2pkh(hash_b);
271            assert_ne!(script_a, script_b);
272        }
273    }
274
275    mod p2sh {
276        use super::*;
277
278        #[test]
279        fn new_p2sh_has_correct_structure() {
280            let script_hash = [0u8; 20];
281            let script = CoreScript::new_p2sh(script_hash);
282            let bytes = script.as_bytes();
283
284            // P2SH script: OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
285            assert_eq!(bytes.len(), 23); // 2 + 20 + 1
286            assert_eq!(bytes[0], opcodes::all::OP_HASH160.to_u8());
287            assert_eq!(bytes[1], opcodes::all::OP_PUSHBYTES_20.to_u8());
288            assert_eq!(&bytes[2..22], &script_hash);
289            assert_eq!(bytes[22], opcodes::all::OP_EQUAL.to_u8());
290        }
291
292        #[test]
293        fn new_p2sh_with_nonzero_hash() {
294            let script_hash: [u8; 20] = [0xFF; 20];
295            let script = CoreScript::new_p2sh(script_hash);
296            let bytes = script.as_bytes();
297            assert_eq!(&bytes[2..22], &script_hash);
298        }
299
300        #[test]
301        fn p2pkh_and_p2sh_differ_for_same_hash() {
302            let hash = [0x42; 20];
303            let p2pkh = CoreScript::new_p2pkh(hash);
304            let p2sh = CoreScript::new_p2sh(hash);
305            assert_ne!(p2pkh, p2sh);
306            // P2PKH is 25 bytes, P2SH is 23 bytes
307            assert_eq!(p2pkh.as_bytes().len(), 25);
308            assert_eq!(p2sh.as_bytes().len(), 23);
309        }
310    }
311
312    mod string_encoding_round_trip {
313        use super::*;
314
315        #[test]
316        fn base64_round_trip() {
317            let original = CoreScript::new_p2pkh([0xAB; 20]);
318            let encoded = original.to_string(Encoding::Base64);
319            let decoded =
320                CoreScript::from_string(&encoded, Encoding::Base64).expect("should decode base64");
321            assert_eq!(original, decoded);
322        }
323
324        #[test]
325        fn hex_round_trip() {
326            let original = CoreScript::new_p2sh([0xCD; 20]);
327            let encoded = original.to_string(Encoding::Hex);
328            let decoded =
329                CoreScript::from_string(&encoded, Encoding::Hex).expect("should decode hex");
330            assert_eq!(original, decoded);
331        }
332
333        #[test]
334        fn from_string_invalid_base64_fails() {
335            let result = CoreScript::from_string("not-valid-base64!!!", Encoding::Base64);
336            assert!(result.is_err());
337        }
338
339        #[test]
340        fn display_uses_base64() {
341            let script = CoreScript::new_p2pkh([0x00; 20]);
342            let display_str = format!("{}", script);
343            let encoded = script.to_string(Encoding::Base64);
344            assert_eq!(display_str, encoded);
345        }
346    }
347
348    mod from_bytes_round_trip {
349        use super::*;
350
351        #[test]
352        fn bytes_round_trip() {
353            let original_bytes = vec![1, 2, 3, 4, 5, 6, 7, 8];
354            let script = CoreScript::from_bytes(original_bytes.clone());
355            assert_eq!(script.as_bytes(), &original_bytes);
356        }
357
358        #[test]
359        fn empty_bytes() {
360            let script = CoreScript::from_bytes(vec![]);
361            assert!(script.as_bytes().is_empty());
362        }
363    }
364
365    mod deref {
366        use super::*;
367
368        #[test]
369        fn deref_returns_inner_script() {
370            let bytes = vec![1, 2, 3];
371            let script = CoreScript::from_bytes(bytes.clone());
372            // Deref gives us access to DashcoreScript methods
373            let inner: &DashcoreScript = &script;
374            assert_eq!(inner.as_bytes(), &bytes);
375        }
376    }
377
378    mod equality_and_clone {
379        use super::*;
380
381        #[test]
382        fn equal_scripts_are_equal() {
383            let a = CoreScript::new_p2pkh([0x11; 20]);
384            let b = CoreScript::new_p2pkh([0x11; 20]);
385            assert_eq!(a, b);
386        }
387
388        #[test]
389        fn different_scripts_are_not_equal() {
390            let a = CoreScript::new_p2pkh([0x11; 20]);
391            let b = CoreScript::new_p2pkh([0x22; 20]);
392            assert_ne!(a, b);
393        }
394
395        #[test]
396        fn clone_produces_equal_script() {
397            let original = CoreScript::new_p2sh([0x33; 20]);
398            let cloned = original.clone();
399            assert_eq!(original, cloned);
400        }
401    }
402
403    mod random_scripts {
404        use super::*;
405        use rand::SeedableRng;
406
407        #[test]
408        fn random_p2pkh_produces_valid_script() {
409            let mut rng = StdRng::seed_from_u64(42);
410            let script = CoreScript::random_p2pkh(&mut rng);
411            let bytes = script.as_bytes();
412            assert_eq!(bytes.len(), 25);
413            assert_eq!(bytes[0], opcodes::all::OP_DUP.to_u8());
414        }
415
416        #[test]
417        fn random_p2sh_produces_valid_script() {
418            let mut rng = StdRng::seed_from_u64(42);
419            let script = CoreScript::random_p2sh(&mut rng);
420            let bytes = script.as_bytes();
421            assert_eq!(bytes.len(), 23);
422            assert_eq!(bytes[0], opcodes::all::OP_HASH160.to_u8());
423        }
424
425        #[test]
426        fn two_random_scripts_differ() {
427            let mut rng = StdRng::seed_from_u64(42);
428            let a = CoreScript::random_p2pkh(&mut rng);
429            let b = CoreScript::random_p2pkh(&mut rng);
430            assert_ne!(a, b);
431        }
432    }
433}