Skip to main content

dpp/identity/state_transition/asset_lock_proof/
mod.rs

1use std::convert::{TryFrom, TryInto};
2
3use dashcore::{OutPoint, Transaction};
4
5use serde::{Deserialize, Deserializer, Serialize};
6
7use bincode::{Decode, Encode};
8
9pub use instant::*;
10use platform_value::Value;
11#[cfg(feature = "validation")]
12use platform_version::version::PlatformVersion;
13use serde::de::Error;
14
15use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
16use crate::prelude::Identifier;
17#[cfg(feature = "validation")]
18use crate::validation::SimpleConsensusValidationResult;
19use crate::{ProtocolError, SerdeParsingError};
20
21pub mod chain;
22pub mod instant;
23pub mod validate_asset_lock_transaction_structure;
24
25// TODO: Serialization with bincode
26// TODO: Consider use Box for InstantAssetLockProof
27//
28// Wire-shape note: this is an *internally-tagged* enum (`#[serde(tag = "type")]`
29// with no `content`). serde's internal tagging works on newtype variants whose
30// inner is a struct — both `InstantAssetLockProof` and `ChainAssetLockProof`
31// qualify — so the inner struct's fields are flattened next to the `type`
32// discriminator: `{"type": "instant", "instantLock": ..., "transaction": ...,
33// "outputIndex": ...}`. This matches the convention applied to other tagged
34// unions exposed to JS (see `AddressWitness`, `AddressFundsFeeStrategyStep`).
35// Bincode `Encode`/`Decode` derives are independent of serde, so consensus
36// binary format is unaffected.
37#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encode, Decode)]
38#[serde(tag = "type", rename_all = "camelCase")]
39#[allow(clippy::large_enum_variant)]
40pub enum AssetLockProof {
41    Instant(#[bincode(with_serde)] InstantAssetLockProof),
42    Chain(#[bincode(with_serde)] ChainAssetLockProof),
43}
44
45/// Wire-shape Deserialize uses the same internal-tag layout the Serialize derive
46/// produces, but routes the instant variant through `RawInstantLockProof` so the
47/// dashcore `InstantLock` can be reconstructed from its raw bytes form.
48#[derive(Deserialize)]
49#[serde(tag = "type", rename_all = "camelCase")]
50enum RawAssetLockProof {
51    Instant(RawInstantLockProof),
52    Chain(ChainAssetLockProof),
53}
54
55impl TryFrom<RawAssetLockProof> for AssetLockProof {
56    type Error = ProtocolError;
57
58    fn try_from(value: RawAssetLockProof) -> Result<Self, Self::Error> {
59        match value {
60            RawAssetLockProof::Instant(raw_instant_lock) => {
61                let instant_lock = raw_instant_lock.try_into()?;
62
63                Ok(AssetLockProof::Instant(instant_lock))
64            }
65            RawAssetLockProof::Chain(chain) => Ok(AssetLockProof::Chain(chain)),
66        }
67    }
68}
69
70impl<'de> Deserialize<'de> for AssetLockProof {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        let raw = RawAssetLockProof::deserialize(deserializer)?;
76        raw.try_into().map_err(|e: ProtocolError| {
77            D::Error::custom(format!(
78                "expected to be able to deserialize asset lock proof: {}",
79                e
80            ))
81        })
82    }
83}
84
85impl Default for AssetLockProof {
86    fn default() -> Self {
87        Self::Instant(InstantAssetLockProof::default())
88    }
89}
90
91impl AsRef<AssetLockProof> for AssetLockProof {
92    fn as_ref(&self) -> &AssetLockProof {
93        self
94    }
95}
96pub enum AssetLockProofType {
97    Instant = 0,
98    Chain = 1,
99}
100
101impl TryFrom<u8> for AssetLockProofType {
102    type Error = SerdeParsingError;
103
104    fn try_from(value: u8) -> Result<Self, Self::Error> {
105        match value {
106            0 => Ok(Self::Instant),
107            1 => Ok(Self::Chain),
108            _ => Err(SerdeParsingError::new("Unexpected asset lock proof type")),
109        }
110    }
111}
112
113impl TryFrom<u64> for AssetLockProofType {
114    type Error = SerdeParsingError;
115
116    fn try_from(value: u64) -> Result<Self, Self::Error> {
117        match value {
118            0 => Ok(Self::Instant),
119            1 => Ok(Self::Chain),
120            _ => Err(SerdeParsingError::new("Unexpected asset lock proof type")),
121        }
122    }
123}
124
125// TODO: Versioning
126impl AssetLockProof {
127    pub fn type_from_raw_value(value: &Value) -> Option<AssetLockProofType> {
128        let proof_type_res = value.get_integer::<u8>("type");
129
130        match proof_type_res {
131            Ok(proof_type_int) => {
132                let proof_type = AssetLockProofType::try_from(proof_type_int);
133                proof_type.ok()
134            }
135            Err(_) => None,
136        }
137    }
138
139    pub fn create_identifier(&self) -> Result<Identifier, ProtocolError> {
140        match self {
141            AssetLockProof::Instant(instant_proof) => instant_proof.create_identifier(),
142            AssetLockProof::Chain(chain_proof) => Ok(chain_proof.create_identifier()),
143        }
144    }
145
146    pub fn output_index(&self) -> u32 {
147        match self {
148            AssetLockProof::Instant(proof) => proof.output_index(),
149            AssetLockProof::Chain(proof) => proof.out_point.vout,
150        }
151    }
152
153    pub fn out_point(&self) -> Option<OutPoint> {
154        match self {
155            AssetLockProof::Instant(proof) => proof.out_point(),
156            AssetLockProof::Chain(proof) => Some(proof.out_point),
157        }
158    }
159
160    pub fn transaction(&self) -> Option<&Transaction> {
161        match self {
162            AssetLockProof::Instant(is_lock) => Some(is_lock.transaction()),
163            AssetLockProof::Chain(_chain_lock) => None,
164        }
165    }
166
167    pub fn to_raw_object(&self) -> Result<Value, ProtocolError> {
168        match self {
169            AssetLockProof::Instant(is) => {
170                platform_value::to_value(is).map_err(ProtocolError::ValueError)
171            }
172            AssetLockProof::Chain(cl) => {
173                platform_value::to_value(cl).map_err(ProtocolError::ValueError)
174            }
175        }
176    }
177
178    /// Validate the structure of the asset lock proof
179    #[cfg(feature = "validation")]
180    pub fn validate_structure(
181        &self,
182        platform_version: &PlatformVersion,
183    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
184        match self {
185            AssetLockProof::Instant(proof) => proof.validate_structure(platform_version),
186            AssetLockProof::Chain(_) => Ok(SimpleConsensusValidationResult::default()),
187        }
188    }
189}
190
191impl TryFrom<&Value> for AssetLockProof {
192    type Error = ProtocolError;
193
194    fn try_from(value: &Value) -> Result<Self, Self::Error> {
195        //this is a complete hack for the moment
196        //todo: replace with
197        //  from_value(value.clone()).map_err(ProtocolError::ValueError)
198        let proof_type_int: Option<u8> = value
199            .get_optional_integer("type")
200            .map_err(ProtocolError::ValueError)?;
201        if let Some(proof_type_int) = proof_type_int {
202            let proof_type = AssetLockProofType::try_from(proof_type_int)?;
203
204            match proof_type {
205                AssetLockProofType::Instant => Ok(Self::Instant(value.clone().try_into()?)),
206                AssetLockProofType::Chain => Ok(Self::Chain(value.clone().try_into()?)),
207            }
208        } else {
209            let map = value.as_map().ok_or(ProtocolError::DecodingError(
210                "error decoding asset lock proof".to_string(),
211            ))?;
212            let (key, asset_lock_value) = map.first().ok_or(ProtocolError::DecodingError(
213                "error decoding asset lock proof as it was empty".to_string(),
214            ))?;
215            match key.as_str().ok_or(ProtocolError::DecodingError(
216                "error decoding asset lock proof".to_string(),
217            ))? {
218                "Instant" => Ok(Self::Instant(asset_lock_value.clone().try_into()?)),
219                "Chain" => Ok(Self::Chain(asset_lock_value.clone().try_into()?)),
220                _ => Err(ProtocolError::DecodingError(
221                    "error decoding asset lock proof".to_string(),
222                )),
223            }
224        }
225    }
226}
227
228impl TryFrom<Value> for AssetLockProof {
229    type Error = ProtocolError;
230
231    fn try_from(value: Value) -> Result<Self, Self::Error> {
232        let proof_type_int: Option<u8> = value
233            .get_optional_integer("type")
234            .map_err(ProtocolError::ValueError)?;
235        if let Some(proof_type_int) = proof_type_int {
236            let proof_type = AssetLockProofType::try_from(proof_type_int)?;
237
238            match proof_type {
239                AssetLockProofType::Instant => Ok(Self::Instant(value.try_into()?)),
240                AssetLockProofType::Chain => Ok(Self::Chain(value.try_into()?)),
241            }
242        } else {
243            let map = value.as_map().ok_or(ProtocolError::DecodingError(
244                "error decoding asset lock proof".to_string(),
245            ))?;
246            let (key, asset_lock_value) = map.first().ok_or(ProtocolError::DecodingError(
247                "error decoding asset lock proof as it was empty".to_string(),
248            ))?;
249            match key.as_str().ok_or(ProtocolError::DecodingError(
250                "error decoding asset lock proof".to_string(),
251            ))? {
252                "Instant" => Ok(Self::Instant(asset_lock_value.clone().try_into()?)),
253                "Chain" => Ok(Self::Chain(asset_lock_value.clone().try_into()?)),
254                _ => Err(ProtocolError::DecodingError(
255                    "error decoding asset lock proof".to_string(),
256                )),
257            }
258        }
259    }
260}
261
262impl TryInto<Value> for AssetLockProof {
263    type Error = ProtocolError;
264
265    fn try_into(self) -> Result<Value, Self::Error> {
266        match self {
267            AssetLockProof::Instant(instant_proof) => {
268                platform_value::to_value(instant_proof).map_err(ProtocolError::ValueError)
269            }
270            AssetLockProof::Chain(chain_proof) => {
271                platform_value::to_value(chain_proof).map_err(ProtocolError::ValueError)
272            }
273        }
274    }
275}
276
277impl TryInto<Value> for &AssetLockProof {
278    type Error = ProtocolError;
279
280    fn try_into(self) -> Result<Value, Self::Error> {
281        match self {
282            AssetLockProof::Instant(instant_proof) => {
283                platform_value::to_value(instant_proof).map_err(ProtocolError::ValueError)
284            }
285            AssetLockProof::Chain(chain_proof) => {
286                platform_value::to_value(chain_proof).map_err(ProtocolError::ValueError)
287            }
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
296    use dashcore::{OutPoint, Txid};
297    use std::str::FromStr;
298
299    /// JSON wire shape is internally tagged: `{type, ...flattened inner fields}`,
300    /// no `data` wrapper. This guards against accidental reintroduction of the
301    /// old adjacent-tagged `{type, data: {...}}` shape and against the divergence
302    /// from the `AddressWitness` / `AddressFundsFeeStrategyStep` precedent.
303    #[test]
304    fn chain_variant_serializes_with_internal_tag() {
305        let txid =
306            Txid::from_str("e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d")
307                .unwrap();
308        let proof = AssetLockProof::Chain(ChainAssetLockProof {
309            core_chain_locked_height: 11,
310            out_point: OutPoint { txid, vout: 1 },
311        });
312
313        let json = serde_json::to_value(&proof).expect("serialize");
314
315        assert_eq!(json["type"], "chain");
316        assert_eq!(json["coreChainLockedHeight"], 11);
317        assert!(
318            json.get("data").is_none(),
319            "should not have a `data` wrapper, got: {}",
320            json
321        );
322
323        // Round-trip
324        let restored: AssetLockProof = serde_json::from_value(json).expect("deserialize");
325        assert_eq!(proof, restored);
326    }
327
328    mod asset_lock_proof_type_try_from {
329        use super::*;
330
331        #[test]
332        fn u8_instant_type() {
333            let proof_type = AssetLockProofType::try_from(0u8).expect("should parse type 0");
334            assert!(matches!(proof_type, AssetLockProofType::Instant));
335        }
336
337        #[test]
338        fn u8_chain_type() {
339            let proof_type = AssetLockProofType::try_from(1u8).expect("should parse type 1");
340            assert!(matches!(proof_type, AssetLockProofType::Chain));
341        }
342
343        #[test]
344        fn u8_invalid_type() {
345            let result = AssetLockProofType::try_from(2u8);
346            assert!(result.is_err());
347        }
348
349        #[test]
350        fn u8_max_invalid_type() {
351            let result = AssetLockProofType::try_from(255u8);
352            assert!(result.is_err());
353        }
354
355        #[test]
356        fn u64_instant_type() {
357            let proof_type = AssetLockProofType::try_from(0u64).expect("should parse type 0");
358            assert!(matches!(proof_type, AssetLockProofType::Instant));
359        }
360
361        #[test]
362        fn u64_chain_type() {
363            let proof_type = AssetLockProofType::try_from(1u64).expect("should parse type 1");
364            assert!(matches!(proof_type, AssetLockProofType::Chain));
365        }
366
367        #[test]
368        fn u64_invalid_type() {
369            let result = AssetLockProofType::try_from(2u64);
370            assert!(result.is_err());
371        }
372
373        #[test]
374        fn u64_large_invalid_type() {
375            let result = AssetLockProofType::try_from(u64::MAX);
376            assert!(result.is_err());
377        }
378    }
379
380    mod chain_asset_lock_proof {
381        use super::*;
382
383        fn make_chain_proof() -> ChainAssetLockProof {
384            ChainAssetLockProof::new(100, [0xAB; 36])
385        }
386
387        #[test]
388        fn chain_proof_construction() {
389            let proof = ChainAssetLockProof::new(42, [0x01; 36]);
390            assert_eq!(proof.core_chain_locked_height, 42);
391        }
392
393        #[test]
394        fn chain_proof_create_identifier_deterministic() {
395            let proof = make_chain_proof();
396            let id1 = proof.create_identifier();
397            let id2 = proof.create_identifier();
398            assert_eq!(id1, id2);
399        }
400
401        #[test]
402        fn different_outpoints_produce_different_identifiers() {
403            let proof_a = ChainAssetLockProof::new(100, [0xAA; 36]);
404            let proof_b = ChainAssetLockProof::new(100, [0xBB; 36]);
405            assert_ne!(proof_a.create_identifier(), proof_b.create_identifier());
406        }
407
408        #[test]
409        fn chain_proof_equality() {
410            let a = ChainAssetLockProof::new(10, [0x01; 36]);
411            let b = ChainAssetLockProof::new(10, [0x01; 36]);
412            assert_eq!(a, b);
413        }
414
415        #[test]
416        fn chain_proof_inequality_height() {
417            let a = ChainAssetLockProof::new(10, [0x01; 36]);
418            let b = ChainAssetLockProof::new(20, [0x01; 36]);
419            assert_ne!(a, b);
420        }
421    }
422
423    mod asset_lock_proof_methods {
424        use super::*;
425
426        fn make_chain_lock_proof() -> AssetLockProof {
427            let chain_proof = ChainAssetLockProof::new(50, [0xCC; 36]);
428            AssetLockProof::Chain(chain_proof)
429        }
430
431        #[test]
432        fn default_is_instant() {
433            let proof = AssetLockProof::default();
434            assert!(matches!(proof, AssetLockProof::Instant(_)));
435        }
436
437        #[test]
438        fn as_ref_returns_self() {
439            let proof = make_chain_lock_proof();
440            let reference: &AssetLockProof = proof.as_ref();
441            assert_eq!(&proof, reference);
442        }
443
444        #[test]
445        fn chain_proof_output_index() {
446            let mut out_point_bytes = [0u8; 36];
447            // Set vout (last 4 bytes in little-endian) to 3
448            out_point_bytes[32] = 3;
449            let chain_proof = ChainAssetLockProof::new(50, out_point_bytes);
450            let proof = AssetLockProof::Chain(chain_proof);
451            assert_eq!(proof.output_index(), 3);
452        }
453
454        #[test]
455        fn chain_proof_out_point_is_some() {
456            let proof = make_chain_lock_proof();
457            assert!(proof.out_point().is_some());
458        }
459
460        #[test]
461        fn chain_proof_transaction_is_none() {
462            let proof = make_chain_lock_proof();
463            assert!(proof.transaction().is_none());
464        }
465
466        #[test]
467        fn chain_proof_to_raw_object() {
468            let proof = make_chain_lock_proof();
469            let result = proof.to_raw_object();
470            assert!(result.is_ok());
471        }
472
473        #[test]
474        fn chain_proof_create_identifier() {
475            let proof = make_chain_lock_proof();
476            let id = proof.create_identifier();
477            assert!(id.is_ok());
478        }
479    }
480
481    mod try_from_value {
482        use super::*;
483
484        #[test]
485        fn chain_proof_value_round_trip() {
486            let chain_proof = ChainAssetLockProof::new(100, [0x42; 36]);
487            let proof = AssetLockProof::Chain(chain_proof);
488
489            // Convert to Value
490            let value: Value = (&proof).try_into().expect("should convert to Value");
491
492            // Now try to read type from value
493            let _type_from_value = AssetLockProof::type_from_raw_value(&value);
494            // Chain proofs serialized via serde may or may not have "type" field depending
495            // on the serialization format. The untagged format may not include it.
496            // What matters is that the conversion itself works.
497
498            // Convert from Value back - this tests the TryFrom<Value> path
499            // with the untagged serde format
500            let raw_value = proof.to_raw_object().expect("should convert to raw object");
501            assert!(!raw_value.is_null());
502        }
503
504        #[test]
505        fn type_from_raw_value_returns_none_for_missing_type() {
506            let value = Value::Map(vec![]);
507            let result = AssetLockProof::type_from_raw_value(&value);
508            assert!(result.is_none());
509        }
510
511        #[test]
512        fn try_from_empty_map_fails() {
513            let value = Value::Map(vec![]);
514            let result = AssetLockProof::try_from(&value);
515            assert!(result.is_err());
516        }
517
518        #[test]
519        fn try_from_value_with_unknown_key_fails() {
520            let value = Value::Map(vec![(
521                Value::Text("Unknown".to_string()),
522                Value::Map(vec![]),
523            )]);
524            let result = AssetLockProof::try_from(&value);
525            assert!(result.is_err());
526        }
527    }
528
529    mod try_into_value {
530        use super::*;
531
532        #[test]
533        fn chain_proof_try_into_value() {
534            let chain_proof = ChainAssetLockProof::new(200, [0xDD; 36]);
535            let proof = AssetLockProof::Chain(chain_proof);
536
537            let value: Result<Value, ProtocolError> = proof.try_into();
538            assert!(value.is_ok());
539        }
540
541        #[test]
542        fn chain_proof_ref_try_into_value() {
543            let chain_proof = ChainAssetLockProof::new(200, [0xDD; 36]);
544            let proof = AssetLockProof::Chain(chain_proof);
545
546            let value: Result<Value, ProtocolError> = (&proof).try_into();
547            assert!(value.is_ok());
548        }
549    }
550}