Skip to main content

dpp/asset_lock/reduced_asset_lock_value/
mod.rs

1use crate::asset_lock::reduced_asset_lock_value::v0::AssetLockValueV0;
2use crate::fee::Credits;
3#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
4use crate::serialization::JsonConvertible;
5#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
6use crate::serialization::ValueConvertible;
7use crate::ProtocolError;
8use bincode::{Decode, DecodeUntrusted, Encode};
9use derive_more::From;
10use platform_serialization_derive::{
11    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
12};
13use platform_value::Bytes32;
14use platform_version::version::PlatformVersion;
15
16mod v0;
17
18pub use v0::{AssetLockValueGettersV0, AssetLockValueSettersV0};
19
20#[derive(
21    Debug,
22    Clone,
23    Encode,
24    Decode,
25    PlatformSerialize,
26    PlatformDeserializeTrusted,
27    PlatformDeserializeUntrusted,
28    From,
29    PartialEq,
30    serde::Serialize,
31    serde::Deserialize,
32    DecodeUntrusted,
33)]
34// Stored asset-lock values are decoded from GroveDB proof elements on the
35// client before the quorum signature is checked, so the byte budget must be
36// enforced by the decoder itself. A valid value is well under 1 KiB (P2PKH
37// script, at most `max_asset_lock_usage_attempts` 32-byte tags); the limit
38// leaves room for Core's 10,000-byte script ceiling.
39#[platform_serialize(limit = 15000, unversioned)]
40#[serde(tag = "$formatVersion")]
41pub enum AssetLockValue {
42    #[serde(rename = "0")]
43    V0(AssetLockValueV0),
44}
45
46#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
47impl JsonConvertible for AssetLockValue {}
48
49#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
50impl ValueConvertible for AssetLockValue {}
51
52impl AssetLockValue {
53    pub fn new(
54        initial_credit_value: Credits,
55        tx_out_script: Vec<u8>,
56        remaining_credit_value: Credits,
57        used_tags: Vec<Bytes32>,
58        platform_version: &PlatformVersion,
59    ) -> Result<Self, ProtocolError> {
60        match platform_version
61            .dpp
62            .asset_lock_versions
63            .reduced_asset_lock_value
64            .default_current_version
65        {
66            0 => Ok(AssetLockValue::V0(AssetLockValueV0 {
67                initial_credit_value,
68                tx_out_script,
69                remaining_credit_value,
70                used_tags,
71            })),
72            version => Err(ProtocolError::UnknownVersionMismatch {
73                method: "ReducedAssetLockValue::new".to_string(),
74                known_versions: vec![0],
75                received: version,
76            }),
77        }
78    }
79}
80
81impl AssetLockValueGettersV0 for AssetLockValue {
82    fn initial_credit_value(&self) -> Credits {
83        match self {
84            AssetLockValue::V0(v0) => v0.initial_credit_value,
85        }
86    }
87
88    fn tx_out_script(&self) -> &Vec<u8> {
89        match self {
90            AssetLockValue::V0(v0) => &v0.tx_out_script,
91        }
92    }
93
94    fn tx_out_script_owned(self) -> Vec<u8> {
95        match self {
96            AssetLockValue::V0(v0) => v0.tx_out_script,
97        }
98    }
99
100    fn remaining_credit_value(&self) -> Credits {
101        match self {
102            AssetLockValue::V0(v0) => v0.remaining_credit_value,
103        }
104    }
105
106    fn used_tags_ref(&self) -> &Vec<Bytes32> {
107        match self {
108            AssetLockValue::V0(v0) => &v0.used_tags,
109        }
110    }
111}
112
113impl AssetLockValueSettersV0 for AssetLockValue {
114    fn set_initial_credit_value(&mut self, value: Credits) {
115        match self {
116            AssetLockValue::V0(v0) => v0.initial_credit_value = value,
117        }
118    }
119
120    fn set_tx_out_script(&mut self, value: Vec<u8>) {
121        match self {
122            AssetLockValue::V0(v0) => v0.tx_out_script = value,
123        }
124    }
125
126    fn set_remaining_credit_value(&mut self, value: Credits) {
127        match self {
128            AssetLockValue::V0(v0) => v0.remaining_credit_value = value,
129        }
130    }
131
132    fn set_used_tags(&mut self, tags: Vec<Bytes32>) {
133        match self {
134            AssetLockValue::V0(v0) => v0.used_tags = tags,
135        }
136    }
137
138    fn add_used_tag(&mut self, tag: Bytes32) {
139        match self {
140            AssetLockValue::V0(v0) => v0.used_tags.push(tag),
141        }
142    }
143}
144
145#[cfg(all(
146    test,
147    feature = "json-conversion",
148    feature = "value-conversion",
149    feature = "serde-conversion"
150))]
151mod json_convertible_tests {
152    use super::*;
153    use platform_value::platform_value;
154    use platform_version::version::PlatformVersion;
155    use serde_json::json;
156
157    fn fixture() -> AssetLockValue {
158        AssetLockValue::new(
159            1_000_000,
160            vec![0xaa, 0xbb, 0xcc, 0xdd],
161            500_000,
162            vec![Bytes32::new([0x42; 32])],
163            PlatformVersion::latest(),
164        )
165        .expect("fixture")
166    }
167
168    #[test]
169    fn json_round_trip_with_full_wire_shape() {
170        use crate::serialization::JsonConvertible;
171        let original = fixture();
172        let json = original.to_json().expect("to_json");
173        // `AssetLockValue` uses the standard `tag = "$formatVersion"`
174        // convention. `Bytes32` is base64 in JSON HR, and `tx_out_script`
175        // (`Vec<u8>`) is base64 too: `#[json_safe_fields]` annotates it with
176        // `serde_bytes_var` (raw bytes in binary, base64 string in JSON).
177        assert_eq!(
178            json,
179            json!({
180                "$formatVersion": "0",
181                "initial_credit_value": 1_000_000,
182                "tx_out_script": "qrvM3Q==",
183                "remaining_credit_value": 500_000,
184                "used_tags": ["QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkI="],
185            })
186        );
187        let recovered = AssetLockValue::from_json(json).expect("from_json");
188        assert_eq!(original, recovered);
189    }
190
191    #[test]
192    fn value_round_trip_with_full_wire_shape() {
193        use crate::serialization::ValueConvertible;
194        use platform_value::Value;
195        let original = fixture();
196        let value = original.to_object().expect("to_object");
197        // `#[json_safe_fields]` annotates `tx_out_script` (`Vec<u8>`) with
198        // `serde_bytes_var`, so it encodes as `Value::Bytes` (raw bytes, not an
199        // array of `U8`). `used_tags` is `Array(Vec<Value::Bytes32>)`.
200        // `initial_credit_value` / `remaining_credit_value` are `Credits` (u64);
201        // in non-human-readable `Value` they stay `Value::U64`.
202        assert_eq!(
203            value,
204            platform_value!({
205                "$formatVersion": "0",
206                "initial_credit_value": 1_000_000u64,
207                "tx_out_script": Value::Bytes(vec![0xaa, 0xbb, 0xcc, 0xdd]),
208                "remaining_credit_value": 500_000u64,
209                "used_tags": [Value::Bytes32([0x42; 32])],
210            })
211        );
212        let recovered = AssetLockValue::from_object(value).expect("from_object");
213        assert_eq!(original, recovered);
214    }
215
216    #[test]
217    fn json_large_credits_serialize_as_strings_for_js_safety() {
218        use crate::serialization::JsonConvertible;
219        // `initial_credit_value` exceeds JS `Number.MAX_SAFE_INTEGER` (2^53 - 1),
220        // so `#[json_safe_fields]` must emit it as a string in human-readable JSON
221        // to avoid silent precision loss when the value crosses into JavaScript.
222        // Without the attribute this serializes as a bare number and the
223        // assertion below fails.
224        let original = AssetLockValue::new(
225            9_007_199_254_740_993, // 2^53 + 1, above MAX_SAFE_INTEGER
226            vec![0xaa, 0xbb, 0xcc, 0xdd],
227            500_000,
228            vec![Bytes32::new([0x42; 32])],
229            PlatformVersion::latest(),
230        )
231        .expect("fixture");
232        let json = original.to_json().expect("to_json");
233        assert_eq!(json["initial_credit_value"], json!("9007199254740993"));
234        // Values within the safe range stay numbers.
235        assert_eq!(json["remaining_credit_value"], json!(500_000));
236        // And the string form round-trips back to the exact u64.
237        let recovered = AssetLockValue::from_json(json).expect("from_json");
238        assert_eq!(original, recovered);
239    }
240}
241
242#[cfg(test)]
243mod deserialize_limit_tests {
244    use super::*;
245    use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
246
247    /// Bincode-encode the V0 shape by hand so the `tx_out_script` length prefix
248    /// can claim more bytes than exist in the payload.
249    fn payload_with_script_length(fake_len: u64) -> Vec<u8> {
250        let config = bincode::config::standard()
251            .with_big_endian()
252            .with_no_limit();
253        let mut buf = Vec::new();
254        // enum discriminant: V0
255        buf.extend_from_slice(&bincode::encode_to_vec(0u32, config).unwrap());
256        // initial_credit_value
257        buf.extend_from_slice(&bincode::encode_to_vec(1_000u64, config).unwrap());
258        // tx_out_script length prefix, with no bytes following it
259        buf.extend_from_slice(&bincode::encode_to_vec(fake_len, config).unwrap());
260        buf
261    }
262
263    /// A proof element is untrusted input: a length prefix must be rejected
264    /// against the byte budget before it sizes an allocation. Without the
265    /// limit this was `vec.resize(8_000_000_000, 0)` and an abort.
266    #[test]
267    fn rejects_script_length_prefix_beyond_budget_without_allocating() {
268        let payload = payload_with_script_length(8_000_000_000);
269        let err = AssetLockValue::deserialize_from_bytes_untrusted(&payload)
270            .expect_err("oversized length prefix must be rejected");
271        assert!(
272            matches!(err, ProtocolError::MaxEncodedBytesReachedError { .. }),
273            "unexpected error: {err}"
274        );
275    }
276
277    /// The largest value the server can legitimately store must stay inside
278    /// the budget on both the encode and decode side, or the node could fail
279    /// to persist it.
280    #[test]
281    fn largest_valid_value_round_trips_under_limit() {
282        let platform_version = PlatformVersion::latest();
283        let max_tags = platform_version
284            .drive_abci
285            .validation_and_processing
286            .state_transitions
287            .max_asset_lock_usage_attempts as usize;
288        let original = AssetLockValue::new(
289            u64::MAX,
290            vec![0xffu8; 10_000],
291            u64::MAX,
292            vec![Bytes32::new([0xff; 32]); max_tags],
293            platform_version,
294        )
295        .expect("value");
296        let bytes = original.serialize_to_bytes().expect("serialize");
297        let recovered =
298            AssetLockValue::deserialize_from_bytes_untrusted(&bytes).expect("deserialize");
299        assert_eq!(original, recovered);
300    }
301}