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
91#[cfg(feature = "json-conversion")]
92impl crate::serialization::JsonConvertible for AssetLockProof {}
93
94#[cfg(feature = "value-conversion")]
95impl crate::serialization::ValueConvertible for AssetLockProof {}
96
97impl AsRef<AssetLockProof> for AssetLockProof {
98    fn as_ref(&self) -> &AssetLockProof {
99        self
100    }
101}
102
103#[cfg(all(
104    test,
105    feature = "json-conversion",
106    feature = "value-conversion",
107    feature = "serde-conversion"
108))]
109mod json_convertible_tests {
110    use super::*;
111    use dashcore::OutPoint;
112    use platform_value::platform_value;
113    use serde_json::json;
114    use std::str::FromStr;
115
116    /// Non-default variant (`Chain` with non-zero core height + a real
117    /// outpoint) so the wire-shape assertion catches silent variant flip /
118    /// inner-zero on round-trip — the previous fixture used `Default::default`
119    /// (`Instant` zero proof).
120    fn fixture() -> AssetLockProof {
121        let out_point = OutPoint::from_str(
122            "0000000000000000000000000000000000000000000000000000000000000001:1",
123        )
124        .expect("outpoint");
125        AssetLockProof::Chain(ChainAssetLockProof {
126            core_chain_locked_height: 12_345,
127            out_point,
128        })
129    }
130
131    #[test]
132    fn json_round_trip_with_full_wire_shape() {
133        use crate::serialization::JsonConvertible;
134        let original = fixture();
135        let json = original.to_json().expect("to_json");
136        // `AssetLockProof` is internally tagged (`#[serde(tag = "$type")]`), so
137        // the inner `ChainAssetLockProof`'s fields are flattened next to the
138        // discriminator. Surprising shape: `OutPoint` has a *string-form*
139        // Serialize impl ("<txid>:<vout>") in dashcore which JSON consumes
140        // as-is — so on the JSON wire, `outPoint` is a single string. The
141        // platform_value layer goes through a different path (see the
142        // value-side test below) and produces a typed Map with `Bytes32` txid
143        // and `U32` vout. `coreChainLockedHeight` is `u32`; JSON erases the
144        // size — see the value-path assertion.
145        assert_eq!(
146            json,
147            json!({
148                "$type": "chain",
149                "coreChainLockedHeight": 12_345,
150                "outPoint": "0000000000000000000000000000000000000000000000000000000000000001:1",
151            })
152        );
153        let recovered = AssetLockProof::from_json(json).expect("from_json");
154        assert_eq!(original, recovered);
155    }
156
157    #[test]
158    fn value_round_trip_with_full_wire_shape() {
159        use crate::serialization::ValueConvertible;
160        let original = fixture();
161        let value = original.to_object().expect("to_object");
162        // platform_value path: `OutPoint` serializes via its derived structural
163        // impl producing a Map { txid: Bytes32, vout: U32 } (NOT the string form
164        // produced on the JSON side). `coreChainLockedHeight` is `u32` so
165        // `12_345u32` locks in `Value::U32`.
166        let mut txid_bytes = [0u8; 32];
167        txid_bytes[0] = 1;
168        assert_eq!(
169            value,
170            platform_value!({
171                "$type": "chain",
172                "coreChainLockedHeight": 12_345u32,
173                "outPoint": {
174                    "txid": platform_value::Value::Bytes32(txid_bytes),
175                    "vout": 1u32,
176                },
177            })
178        );
179        let recovered = AssetLockProof::from_object(value).expect("from_object");
180        assert_eq!(original, recovered);
181    }
182}
183pub enum AssetLockProofType {
184    Instant = 0,
185    Chain = 1,
186}
187
188impl TryFrom<u8> for AssetLockProofType {
189    type Error = SerdeParsingError;
190
191    fn try_from(value: u8) -> Result<Self, Self::Error> {
192        match value {
193            0 => Ok(Self::Instant),
194            1 => Ok(Self::Chain),
195            _ => Err(SerdeParsingError::new("Unexpected asset lock proof type")),
196        }
197    }
198}
199
200impl TryFrom<u64> for AssetLockProofType {
201    type Error = SerdeParsingError;
202
203    fn try_from(value: u64) -> Result<Self, Self::Error> {
204        match value {
205            0 => Ok(Self::Instant),
206            1 => Ok(Self::Chain),
207            _ => Err(SerdeParsingError::new("Unexpected asset lock proof type")),
208        }
209    }
210}
211
212// TODO: Versioning
213impl AssetLockProof {
214    pub fn type_from_raw_value(value: &Value) -> Option<AssetLockProofType> {
215        let proof_type_res = value.get_integer::<u8>("type");
216
217        match proof_type_res {
218            Ok(proof_type_int) => {
219                let proof_type = AssetLockProofType::try_from(proof_type_int);
220                proof_type.ok()
221            }
222            Err(_) => None,
223        }
224    }
225
226    pub fn create_identifier(&self) -> Result<Identifier, ProtocolError> {
227        match self {
228            AssetLockProof::Instant(instant_proof) => instant_proof.create_identifier(),
229            AssetLockProof::Chain(chain_proof) => Ok(chain_proof.create_identifier()),
230        }
231    }
232
233    pub fn output_index(&self) -> u32 {
234        match self {
235            AssetLockProof::Instant(proof) => proof.output_index(),
236            AssetLockProof::Chain(proof) => proof.out_point.vout,
237        }
238    }
239
240    pub fn out_point(&self) -> Option<OutPoint> {
241        match self {
242            AssetLockProof::Instant(proof) => proof.out_point(),
243            AssetLockProof::Chain(proof) => Some(proof.out_point),
244        }
245    }
246
247    pub fn transaction(&self) -> Option<&Transaction> {
248        match self {
249            AssetLockProof::Instant(is_lock) => Some(is_lock.transaction()),
250            AssetLockProof::Chain(_chain_lock) => None,
251        }
252    }
253
254    /// Validate the structure of the asset lock proof
255    #[cfg(feature = "validation")]
256    pub fn validate_structure(
257        &self,
258        platform_version: &PlatformVersion,
259    ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
260        match self {
261            AssetLockProof::Instant(proof) => proof.validate_structure(platform_version),
262            AssetLockProof::Chain(_) => Ok(SimpleConsensusValidationResult::default()),
263        }
264    }
265}
266
267// Canonical `TryFrom<Value> for AssetLockProof` is provided via the
268// `Deserialize` impl above (which routes through `RawAssetLockProof` for
269// the instant-lock raw-bytes shape) and `platform_value::from_value`. The
270// previous hack here accepted legacy integer-tagged
271// (`{type: 0|1, ...fields}`) and externally-tagged
272// (`{Instant: {...}}`) shapes — both predated the
273// `#[serde(tag = "$type")]` Critical-2 fix. Audit (Phase D step 6)
274// confirmed all currently-flowing values are canonical-tagged
275// (string `type`), so the hacks were dead.
276
277impl TryFrom<&Value> for AssetLockProof {
278    type Error = ProtocolError;
279
280    fn try_from(value: &Value) -> Result<Self, Self::Error> {
281        platform_value::from_value(value.clone()).map_err(ProtocolError::ValueError)
282    }
283}
284
285impl TryFrom<Value> for AssetLockProof {
286    type Error = ProtocolError;
287
288    fn try_from(value: Value) -> Result<Self, Self::Error> {
289        platform_value::from_value(value).map_err(ProtocolError::ValueError)
290    }
291}
292
293// `TryInto<Value>` impls (and the inherent `to_raw_object` that mirrored
294// them) used to live here, producing *untagged* `Value` (drops the variant
295// tag entirely). They were structurally asymmetric with the canonical
296// Deserialize, which expects the `type: "instant" | "chain"` discriminator
297// to route through `RawAssetLockProof`. Confirmed zero production callers,
298// so deleted in Phase D step 6. Use canonical `ValueConvertible::to_object`
299// — it produces the correctly-tagged shape that `Deserialize` accepts on
300// the way back.
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
306    use dashcore::{OutPoint, Txid};
307    use std::str::FromStr;
308
309    /// JSON wire shape is internally tagged: `{type, ...flattened inner fields}`,
310    /// no `data` wrapper. This guards against accidental reintroduction of the
311    /// old adjacent-tagged `{type, data: {...}}` shape and against the divergence
312    /// from the `AddressWitness` / `AddressFundsFeeStrategyStep` precedent.
313    #[test]
314    fn chain_variant_serializes_with_internal_tag() {
315        let txid =
316            Txid::from_str("e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d")
317                .unwrap();
318        let proof = AssetLockProof::Chain(ChainAssetLockProof {
319            core_chain_locked_height: 11,
320            out_point: OutPoint { txid, vout: 1 },
321        });
322
323        let json = serde_json::to_value(&proof).expect("serialize");
324
325        assert_eq!(json["$type"], "chain");
326        assert_eq!(json["coreChainLockedHeight"], 11);
327        assert!(
328            json.get("data").is_none(),
329            "should not have a `data` wrapper, got: {}",
330            json
331        );
332
333        // Round-trip
334        let restored: AssetLockProof = serde_json::from_value(json).expect("deserialize");
335        assert_eq!(proof, restored);
336    }
337
338    mod asset_lock_proof_type_try_from {
339        use super::*;
340
341        #[test]
342        fn u8_instant_type() {
343            let proof_type = AssetLockProofType::try_from(0u8).expect("should parse type 0");
344            assert!(matches!(proof_type, AssetLockProofType::Instant));
345        }
346
347        #[test]
348        fn u8_chain_type() {
349            let proof_type = AssetLockProofType::try_from(1u8).expect("should parse type 1");
350            assert!(matches!(proof_type, AssetLockProofType::Chain));
351        }
352
353        #[test]
354        fn u8_invalid_type() {
355            let result = AssetLockProofType::try_from(2u8);
356            assert!(result.is_err());
357        }
358
359        #[test]
360        fn u8_max_invalid_type() {
361            let result = AssetLockProofType::try_from(255u8);
362            assert!(result.is_err());
363        }
364
365        #[test]
366        fn u64_instant_type() {
367            let proof_type = AssetLockProofType::try_from(0u64).expect("should parse type 0");
368            assert!(matches!(proof_type, AssetLockProofType::Instant));
369        }
370
371        #[test]
372        fn u64_chain_type() {
373            let proof_type = AssetLockProofType::try_from(1u64).expect("should parse type 1");
374            assert!(matches!(proof_type, AssetLockProofType::Chain));
375        }
376
377        #[test]
378        fn u64_invalid_type() {
379            let result = AssetLockProofType::try_from(2u64);
380            assert!(result.is_err());
381        }
382
383        #[test]
384        fn u64_large_invalid_type() {
385            let result = AssetLockProofType::try_from(u64::MAX);
386            assert!(result.is_err());
387        }
388    }
389
390    mod chain_asset_lock_proof {
391        use super::*;
392
393        fn make_chain_proof() -> ChainAssetLockProof {
394            ChainAssetLockProof::new(100, [0xAB; 36])
395        }
396
397        #[test]
398        fn chain_proof_construction() {
399            let proof = ChainAssetLockProof::new(42, [0x01; 36]);
400            assert_eq!(proof.core_chain_locked_height, 42);
401        }
402
403        #[test]
404        fn chain_proof_create_identifier_deterministic() {
405            let proof = make_chain_proof();
406            let id1 = proof.create_identifier();
407            let id2 = proof.create_identifier();
408            assert_eq!(id1, id2);
409        }
410
411        #[test]
412        fn different_outpoints_produce_different_identifiers() {
413            let proof_a = ChainAssetLockProof::new(100, [0xAA; 36]);
414            let proof_b = ChainAssetLockProof::new(100, [0xBB; 36]);
415            assert_ne!(proof_a.create_identifier(), proof_b.create_identifier());
416        }
417
418        #[test]
419        fn chain_proof_equality() {
420            let a = ChainAssetLockProof::new(10, [0x01; 36]);
421            let b = ChainAssetLockProof::new(10, [0x01; 36]);
422            assert_eq!(a, b);
423        }
424
425        #[test]
426        fn chain_proof_inequality_height() {
427            let a = ChainAssetLockProof::new(10, [0x01; 36]);
428            let b = ChainAssetLockProof::new(20, [0x01; 36]);
429            assert_ne!(a, b);
430        }
431    }
432
433    mod asset_lock_proof_methods {
434        use super::*;
435
436        fn make_chain_lock_proof() -> AssetLockProof {
437            let chain_proof = ChainAssetLockProof::new(50, [0xCC; 36]);
438            AssetLockProof::Chain(chain_proof)
439        }
440
441        #[test]
442        fn default_is_instant() {
443            let proof = AssetLockProof::default();
444            assert!(matches!(proof, AssetLockProof::Instant(_)));
445        }
446
447        #[test]
448        fn as_ref_returns_self() {
449            let proof = make_chain_lock_proof();
450            let reference: &AssetLockProof = proof.as_ref();
451            assert_eq!(&proof, reference);
452        }
453
454        #[test]
455        fn chain_proof_output_index() {
456            let mut out_point_bytes = [0u8; 36];
457            // Set vout (last 4 bytes in little-endian) to 3
458            out_point_bytes[32] = 3;
459            let chain_proof = ChainAssetLockProof::new(50, out_point_bytes);
460            let proof = AssetLockProof::Chain(chain_proof);
461            assert_eq!(proof.output_index(), 3);
462        }
463
464        #[test]
465        fn chain_proof_out_point_is_some() {
466            let proof = make_chain_lock_proof();
467            assert!(proof.out_point().is_some());
468        }
469
470        #[test]
471        fn chain_proof_transaction_is_none() {
472            let proof = make_chain_lock_proof();
473            assert!(proof.transaction().is_none());
474        }
475
476        #[test]
477        fn chain_proof_to_object_canonical() {
478            // After Phase D step 6, `to_raw_object` (which produced an
479            // untagged Value) was deleted. Canonical
480            // `ValueConvertible::to_object` produces the correctly-tagged
481            // shape that round-trips through `Deserialize`.
482            use crate::serialization::ValueConvertible;
483            let proof = make_chain_lock_proof();
484            let result = proof.to_object();
485            assert!(result.is_ok());
486        }
487
488        #[test]
489        fn chain_proof_create_identifier() {
490            let proof = make_chain_lock_proof();
491            let id = proof.create_identifier();
492            assert!(id.is_ok());
493        }
494    }
495
496    mod try_from_value {
497        use super::*;
498
499        #[test]
500        fn chain_proof_value_round_trip() {
501            // Canonical `ValueConvertible::to_object` produces a tagged
502            // Value (`{type: "chain", coreChainLockedHeight: ..., outPoint: ...}`)
503            // that round-trips through the manual `Deserialize` (which routes
504            // via `RawAssetLockProof`).
505            use crate::serialization::ValueConvertible;
506            let chain_proof = ChainAssetLockProof::new(100, [0x42; 36]);
507            let proof = AssetLockProof::Chain(chain_proof);
508
509            let value = proof.to_object().expect("to_object");
510            // The canonical `to_object` produces `type: "chain"` in the
511            // wire shape. `type_from_raw_value` expects an integer-typed
512            // tag (legacy shape), so it returns None on canonical output —
513            // confirm via the serde Map directly instead.
514            let map = value.to_map_ref().expect("map");
515            assert_eq!(
516                map.iter()
517                    .find_map(|(k, v)| (k.as_text() == Some("$type")).then(|| v.as_text())),
518                Some(Some("chain"))
519            );
520
521            let recovered =
522                AssetLockProof::from_object(value).expect("from_object should round-trip");
523            assert_eq!(proof, recovered);
524        }
525
526        #[test]
527        fn type_from_raw_value_returns_none_for_missing_type() {
528            let value = Value::Map(vec![]);
529            let result = AssetLockProof::type_from_raw_value(&value);
530            assert!(result.is_none());
531        }
532
533        #[test]
534        fn try_from_empty_map_fails() {
535            let value = Value::Map(vec![]);
536            let result = AssetLockProof::try_from(&value);
537            assert!(result.is_err());
538        }
539
540        #[test]
541        fn try_from_value_with_unknown_key_fails() {
542            let value = Value::Map(vec![(
543                Value::Text("Unknown".to_string()),
544                Value::Map(vec![]),
545            )]);
546            let result = AssetLockProof::try_from(&value);
547            assert!(result.is_err());
548        }
549    }
550
551    // The `try_into_value` module previously exercised the now-deleted
552    // `TryInto<Value>` impls (which produced untagged `Value`). Canonical
553    // `ValueConvertible::to_object` is exercised in `try_from_value` above.
554}