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