Skip to main content

dpp/state_transition/
proof_result.rs

1use crate::address_funds::PlatformAddress;
2use crate::asset_lock::StoredAssetLockInfo;
3use crate::balances::credits::TokenAmount;
4use crate::data_contract::group::GroupSumPower;
5use crate::data_contract::DataContract;
6use crate::document::Document;
7use crate::fee::Credits;
8use crate::group::group_action_status::GroupActionStatus;
9use crate::identity::{Identity, PartialIdentity};
10use crate::prelude::AddressNonce;
11use crate::tokens::info::IdentityTokenInfo;
12use crate::tokens::status::TokenStatus;
13use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
14use crate::voting::votes::Vote;
15use platform_value::Identifier;
16use std::collections::BTreeMap;
17
18#[derive(Debug, PartialEq, strum::Display, derive_more::TryInto)]
19#[cfg_attr(
20    feature = "serde-conversion",
21    derive(serde::Serialize, serde::Deserialize)
22)]
23pub enum StateTransitionProofResult {
24    VerifiedDataContract(DataContract),
25    VerifiedIdentity(Identity),
26    VerifiedTokenBalanceAbsence(Identifier),
27    // `TokenAmount`/`Credits` (u64) live in tuple variants / nested containers
28    // that `#[json_safe_fields]` can't reach; apply the JS-safe helpers directly
29    // so values above `MAX_SAFE_INTEGER` serialize as strings in human-readable
30    // JSON. (`AddressNonce`/`GroupSumPower` are `u32` → already JS-safe; the
31    // `Vec<u8>` nullifiers are arrays of bytes < 256 → no precision concern.)
32    VerifiedTokenBalance(
33        Identifier,
34        #[cfg_attr(
35            feature = "json-conversion",
36            serde(with = "crate::serialization::json_safe_u64")
37        )]
38        TokenAmount,
39    ),
40    VerifiedTokenIdentityInfo(Identifier, IdentityTokenInfo),
41    VerifiedTokenPricingSchedule(Identifier, Option<TokenPricingSchedule>),
42    VerifiedTokenStatus(TokenStatus),
43    VerifiedTokenIdentitiesBalances(
44        #[cfg_attr(
45            feature = "json-conversion",
46            serde(
47                with = "crate::serialization::json::safe_integer_map::json_safe_identifier_u64_map"
48            )
49        )]
50        BTreeMap<Identifier, TokenAmount>,
51    ),
52    VerifiedPartialIdentity(PartialIdentity),
53    VerifiedBalanceTransfer(PartialIdentity, PartialIdentity), //from/to
54    VerifiedDocuments(BTreeMap<Identifier, Option<Document>>),
55    VerifiedTokenActionWithDocument(Document),
56    VerifiedTokenGroupActionWithDocument(GroupSumPower, Option<Document>),
57    VerifiedTokenGroupActionWithTokenBalance(
58        GroupSumPower,
59        GroupActionStatus,
60        #[cfg_attr(
61            feature = "json-conversion",
62            serde(with = "crate::serialization::json_safe_option_u64")
63        )]
64        Option<TokenAmount>,
65    ),
66    VerifiedTokenGroupActionWithTokenIdentityInfo(
67        GroupSumPower,
68        GroupActionStatus,
69        Option<IdentityTokenInfo>,
70    ),
71    VerifiedTokenGroupActionWithTokenPricingSchedule(
72        GroupSumPower,
73        GroupActionStatus,
74        Option<TokenPricingSchedule>,
75    ),
76    VerifiedMasternodeVote(Vote),
77    VerifiedNextDistribution(Vote),
78    VerifiedAddressInfos(
79        #[cfg_attr(
80            feature = "json-conversion",
81            serde(with = "json_safe_address_info_map")
82        )]
83        BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>,
84    ),
85    VerifiedIdentityFullWithAddressInfos(
86        Identity,
87        #[cfg_attr(
88            feature = "json-conversion",
89            serde(with = "json_safe_address_info_map")
90        )]
91        BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>,
92    ),
93    VerifiedIdentityWithAddressInfos(
94        PartialIdentity,
95        #[cfg_attr(
96            feature = "json-conversion",
97            serde(with = "json_safe_address_info_map")
98        )]
99        BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>,
100    ),
101    VerifiedAssetLockConsumed(StoredAssetLockInfo),
102    VerifiedShieldedNullifiers(Vec<(Vec<u8>, bool)>),
103    VerifiedShieldedNullifiersWithAddressInfos(
104        Vec<(Vec<u8>, bool)>,
105        #[cfg_attr(
106            feature = "json-conversion",
107            serde(with = "json_safe_address_info_map")
108        )]
109        BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>,
110    ),
111    VerifiedShieldedNullifiersWithWithdrawalDocument(
112        Vec<(Vec<u8>, bool)>,
113        BTreeMap<Identifier, Option<Document>>,
114    ),
115    /// Returned by `ShieldFromAssetLock` when a `surplus_output` is set. Carries the consumed
116    /// asset-lock info AND the proven balance of the surplus-output address, so a light/SDK
117    /// client can cryptographically confirm the asset-lock surplus credit landed at the signed
118    /// `surplus_output` address. The plain [`VerifiedAssetLockConsumed`] is still returned when
119    /// no `surplus_output` is set.
120    ///
121    /// [`VerifiedAssetLockConsumed`]: StateTransitionProofResult::VerifiedAssetLockConsumed
122    VerifiedAssetLockConsumedWithAddressInfos(
123        StoredAssetLockInfo,
124        BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>,
125    ),
126    /// Returned by `IdentityCreateFromShieldedPool`. Carries the newly-created [`Identity`] AND the
127    /// presence of each spent nullifier (`(nullifier_bytes, present)`), proven together in a single
128    /// STRICT merged multi-root GroveDB proof. A light/SDK client can cryptographically confirm both
129    /// that the identity was created and that the funding nullifiers were consumed.
130    VerifiedIdentityWithShieldedNullifiers(Identity, Vec<(Vec<u8>, bool)>),
131}
132
133/// A verified state-transition proof result, tagged with the guarantee the
134/// proof establishes.
135///
136/// Some transition families (balance top-ups, credit transfers and
137/// withdrawals, address funds movements, shields, no-history token
138/// operations) produce proofs whose values cannot be bound to the execution
139/// of one specific transition: the proof only authenticates the affected
140/// keys' state at the committed block. The tag makes that distinction part
141/// of the type so a snapshot cannot be mistaken for execution evidence.
142#[derive(Debug, PartialEq, strum::Display)]
143#[cfg_attr(
144    feature = "serde-conversion",
145    derive(serde::Serialize, serde::Deserialize)
146)]
147pub enum StateTransitionProofOutcome {
148    /// The proof binds the execution of this specific state transition:
149    /// the verified values could only exist if the transition was applied.
150    ExecutionProved(StateTransitionProofResult),
151    /// The proof authenticates a snapshot of the state the transition
152    /// affects — keys derived from the transition, values as of the proof's
153    /// block — but cannot bind them to the execution of this transition.
154    /// Treat as a height-pinned snapshot, not as evidence of execution.
155    AffectedState(StateTransitionProofResult),
156}
157
158impl StateTransitionProofOutcome {
159    /// The verified result, regardless of the guarantee tag.
160    pub fn result(&self) -> &StateTransitionProofResult {
161        match self {
162            Self::ExecutionProved(result) | Self::AffectedState(result) => result,
163        }
164    }
165
166    /// Consume the outcome, discarding the guarantee tag.
167    pub fn into_result(self) -> StateTransitionProofResult {
168        match self {
169            Self::ExecutionProved(result) | Self::AffectedState(result) => result,
170        }
171    }
172
173    /// Whether the proof established that this specific transition executed.
174    pub fn is_execution_proved(&self) -> bool {
175        matches!(self, Self::ExecutionProved(_))
176    }
177}
178
179/// Serde `with` module for `BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>`.
180///
181/// `AddressNonce` is `u32` (JS-safe); `Credits` is `u64` and must serialize as a
182/// string in human-readable JSON above `MAX_SAFE_INTEGER`. A small wrapper tuple
183/// carries `#[serde(with = "json_safe_u64")]` on the credits, preserving the
184/// `[nonce, credits]` array wire-shape while making the value JS-safe. Binary /
185/// `Value` paths stay native (the helper checks `is_human_readable`).
186#[cfg(feature = "json-conversion")]
187mod json_safe_address_info_map {
188    use super::{AddressNonce, Credits, PlatformAddress};
189    use serde::de::Deserializer;
190    use serde::ser::{SerializeMap, Serializer};
191    use serde::{Deserialize, Serialize};
192    use std::collections::BTreeMap;
193
194    /// The address-info map shape shared by several `StateTransitionProofResult`
195    /// variants. Aliased to keep the helper signatures below under clippy's
196    /// `type_complexity` threshold.
197    type AddressInfoMap = BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>>;
198
199    #[derive(Serialize, Deserialize)]
200    struct Entry(
201        AddressNonce,
202        #[serde(with = "crate::serialization::json_safe_u64")] Credits,
203    );
204
205    pub fn serialize<S: Serializer>(
206        map: &AddressInfoMap,
207        serializer: S,
208    ) -> Result<S::Ok, S::Error> {
209        let mut s = serializer.serialize_map(Some(map.len()))?;
210        for (k, v) in map {
211            let wrapped = v.map(|(nonce, credits)| Entry(nonce, credits));
212            s.serialize_entry(k, &wrapped)?;
213        }
214        s.end()
215    }
216
217    pub fn deserialize<'de, D: Deserializer<'de>>(
218        deserializer: D,
219    ) -> Result<AddressInfoMap, D::Error> {
220        let raw: BTreeMap<PlatformAddress, Option<Entry>> = BTreeMap::deserialize(deserializer)?;
221        Ok(raw
222            .into_iter()
223            .map(|(k, v)| (k, v.map(|Entry(nonce, credits)| (nonce, credits))))
224            .collect())
225    }
226}
227
228// --- canonical conversion trait impls (unification pass 1) ---
229#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
230impl crate::serialization::JsonConvertible for StateTransitionProofResult {}
231
232#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
233impl crate::serialization::ValueConvertible for StateTransitionProofResult {}
234
235#[cfg(all(
236    test,
237    feature = "json-conversion",
238    feature = "value-conversion",
239    feature = "serde-conversion"
240))]
241mod json_convertible_tests {
242    use super::*;
243    use platform_value::{Identifier, Value};
244    use serde_json::json;
245
246    /// Non-default variant `VerifiedTokenBalance(id, amount)` with both
247    /// tuple fields set so the wire-shape assertion catches silent variant
248    /// flip / inner-zero on round-trip.
249    fn fixture() -> StateTransitionProofResult {
250        StateTransitionProofResult::VerifiedTokenBalance(
251            Identifier::new([0xab; 32]),
252            123_456_789u64,
253        )
254    }
255
256    #[test]
257    fn json_round_trip_with_full_wire_shape() {
258        use crate::serialization::JsonConvertible;
259        let original = fixture();
260        let json = original.to_json().expect("to_json");
261        // `StateTransitionProofResult` uses serde external tagging (default,
262        // no `#[serde(tag = ...)]`). Tuple variants serialize as
263        // `{ "VariantName": [field0, field1, ...] }`. `Identifier` -> base58
264        // string in JSON; `TokenAmount` is `u64` and JSON erases the size —
265        // see the value-path assertion which uses `123_456_789u64`.
266        assert_eq!(
267            json,
268            json!({
269                "VerifiedTokenBalance": [
270                    "CZ8YUVdk7znjrUmnb5n7kgySk9yRAsQDYmyCxzfSky9t",
271                    123_456_789u64,
272                ],
273            })
274        );
275        let recovered = StateTransitionProofResult::from_json(json).expect("from_json");
276        assert_eq!(original, recovered);
277    }
278
279    #[test]
280    fn value_round_trip_with_full_wire_shape() {
281        use crate::serialization::ValueConvertible;
282        let original = fixture();
283        let value = original.to_object().expect("to_object");
284        // platform_value preserves typed `Identifier` and `U64` variants. We
285        // construct the expected `Value::Map` by hand: `platform_value!{...}`
286        // would convert the `Identifier` interpolation through Serialize
287        // (correct) but the outer shape has only one (Text-keyed) entry whose
288        // value is an Array of mixed-typed Values, so it's clearer to write
289        // the literal Map.
290        let expected = Value::Map(vec![(
291            Value::Text("VerifiedTokenBalance".to_string()),
292            Value::Array(vec![Value::Identifier([0xab; 32]), Value::U64(123_456_789)]),
293        )]);
294        assert_eq!(value, expected);
295        let recovered = StateTransitionProofResult::from_object(value).expect("from_object");
296        assert_eq!(original, recovered);
297    }
298
299    #[test]
300    fn verified_token_balance_large_amount_serializes_as_string() {
301        use crate::serialization::JsonConvertible;
302        // `TokenAmount` above `Number.MAX_SAFE_INTEGER` must serialize as a JSON
303        // string (it sits in a tuple variant the macro can't reach).
304        let original = StateTransitionProofResult::VerifiedTokenBalance(
305            Identifier::new([0xab; 32]),
306            9_007_199_254_740_993, // 2^53 + 1
307        );
308        let json = original.to_json().expect("to_json");
309        assert_eq!(json["VerifiedTokenBalance"][1], json!("9007199254740993"));
310        let recovered = StateTransitionProofResult::from_json(json).expect("from_json");
311        assert_eq!(original, recovered);
312    }
313
314    #[test]
315    fn verified_address_infos_large_credits_serialize_as_string() {
316        use crate::serialization::{JsonConvertible, ValueConvertible};
317        use std::collections::BTreeMap;
318        // `Credits` (u64) nested in `BTreeMap<PlatformAddress, Option<(AddressNonce,
319        // Credits)>>` must be JS-safe via the bespoke `json_safe_address_info_map`
320        // helper, preserving the `[nonce, credits]` array wire-shape.
321        let big_credits: Credits = 9_007_199_254_740_993; // 2^53 + 1
322        let mut infos: BTreeMap<PlatformAddress, Option<(AddressNonce, Credits)>> = BTreeMap::new();
323        infos.insert(PlatformAddress::P2pkh([0x11; 20]), Some((7, big_credits)));
324        infos.insert(PlatformAddress::P2sh([0x22; 20]), None);
325        let original = StateTransitionProofResult::VerifiedAddressInfos(infos);
326
327        // Human-readable JSON: credits string, nonce number, null preserved.
328        let json = original.to_json().expect("to_json");
329        let map = json["VerifiedAddressInfos"].as_object().expect("object");
330        let non_null = map
331            .values()
332            .find(|v| !v.is_null())
333            .expect("one populated entry");
334        assert_eq!(non_null[0], json!(7));
335        assert_eq!(non_null[1], json!("9007199254740993"));
336        assert!(
337            map.values().any(|v| v.is_null()),
338            "the None entry must survive"
339        );
340        let recovered = StateTransitionProofResult::from_json(json).expect("from_json");
341        assert_eq!(original, recovered);
342
343        // Non-human-readable (platform_value): native u64, round-trips intact.
344        let value = original.to_object().expect("to_object");
345        let recovered = StateTransitionProofResult::from_object(value).expect("from_object");
346        assert_eq!(original, recovered);
347    }
348}