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