Skip to main content

dpp/identity/
identity.rs

1#[cfg(feature = "state-transitions")]
2use crate::address_funds::PlatformAddress;
3use crate::identity::v0::IdentityV0;
4use crate::identity::{IdentityPublicKey, KeyID};
5#[cfg(feature = "state-transitions")]
6use crate::prelude::AddressNonce;
7use crate::prelude::Revision;
8#[cfg(feature = "json-conversion")]
9use crate::serialization::json_safe_fields;
10#[cfg(feature = "json-conversion")]
11use crate::serialization::JsonConvertible;
12#[cfg(feature = "value-conversion")]
13use crate::serialization::ValueConvertible;
14
15#[cfg(feature = "identity-hashing")]
16use crate::serialization::PlatformSerializable;
17#[cfg(feature = "identity-hashing")]
18use crate::util::hash;
19use crate::version::PlatformVersion;
20
21use crate::ProtocolError;
22#[cfg(feature = "identity-serialization")]
23use bincode::{Decode, DecodeUntrusted, Encode};
24use derive_more::From;
25#[cfg(feature = "identity-serialization")]
26use platform_serialization_derive::{
27    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
28};
29use platform_value::Identifier;
30
31use crate::fee::Credits;
32use std::collections::{BTreeMap, BTreeSet};
33
34/// The identity is not stored inside of drive, because of this, the serialization is mainly for
35/// transport, the serialization of the identity will include the version, so no passthrough or
36/// untagged is needed here
37#[derive(Debug, Clone, PartialEq, From)]
38#[cfg_attr(
39   feature = "serde-conversion",
40    derive(serde::Serialize, serde::Deserialize),
41    serde(tag = "$formatVersion"),
42    // platform_version_path("dpp.identity_versions.identity_structure_version")
43)]
44#[cfg_attr(
45    feature = "identity-serialization",
46    derive(
47        Encode,
48        Decode,
49        DecodeUntrusted,
50        PlatformDeserializeTrusted,
51        PlatformDeserializeUntrusted,
52        PlatformSerialize
53    ),
54    platform_serialize(limit = 15000, unversioned)
55)]
56#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
57pub enum Identity {
58    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
59    V0(IdentityV0),
60}
61
62#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
63impl JsonConvertible for Identity {}
64
65#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
66impl JsonConvertible for PartialIdentity {}
67
68#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
69impl ValueConvertible for PartialIdentity {}
70
71#[cfg(all(
72    test,
73    feature = "json-conversion",
74    feature = "value-conversion",
75    feature = "serde-conversion"
76))]
77mod json_convertible_tests {
78    use super::*;
79    use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
80    use crate::identity::{KeyType, Purpose, SecurityLevel};
81    use platform_value::{platform_value, BinaryData, Value};
82    use serde_json::json;
83
84    fn fixture_pubkey(id: u32, byte: u8) -> IdentityPublicKey {
85        IdentityPublicKey::V0(IdentityPublicKeyV0 {
86            id,
87            key_type: KeyType::ECDSA_SECP256K1,
88            purpose: Purpose::AUTHENTICATION,
89            security_level: SecurityLevel::MASTER,
90            contract_bounds: None,
91            read_only: false,
92            data: BinaryData::new(vec![byte; 33]),
93            disabled_at: None,
94        })
95    }
96
97    fn fixture() -> Identity {
98        let mut public_keys = BTreeMap::new();
99        public_keys.insert(0, fixture_pubkey(0, 0xa0));
100        public_keys.insert(1, fixture_pubkey(1, 0xb1));
101        Identity::V0(IdentityV0 {
102            id: Identifier::new([0x42; 32]),
103            public_keys,
104            balance: 1_000_000,
105            revision: 7,
106        })
107    }
108
109    #[test]
110    fn json_round_trip_with_full_wire_shape() {
111        use crate::serialization::JsonConvertible;
112        let original = fixture();
113        let json = original.to_json().expect("to_json");
114        // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has
115        // `rename_all = "camelCase"`. `public_keys` uses a custom serde wrapper
116        // that emits a `Vec` of `IdentityPublicKey` values (keys dropped, then
117        // reconstructed on deserialize from each key's `id`). Each
118        // `IdentityPublicKey` is itself an internally-tagged enum, so the inner
119        // wire shape mirrors the per-key test in
120        // `identity_public_key::mod::json_convertible_tests`.
121        // Sized-int fields with JSON loss:
122        // - `balance`: u64 (Credits)
123        // - `revision`: u64 (Revision)
124        // - inner `id`: u32, `purpose`/`securityLevel`/`type`: u8 reprs.
125        // `Identifier` serializes as base58 in JSON; `BinaryData` as base64.
126        // Purpose::AUTHENTICATION = 0, KeyType::ECDSA_SECP256K1 = 0,
127        // SecurityLevel::MASTER = 0.
128        assert_eq!(
129            json,
130            json!({
131                "$formatVersion": "0",
132                "id": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf",
133                // After Phase D step 4, `disabled_at` carries
134                // `#[serde(skip_serializing_if = "Option::is_none")]`, so
135                // non-disabled keys no longer emit `disabledAt: null`.
136                "publicKeys": [
137                    {
138                        "$formatVersion": "0",
139                        "id": 0,
140                        "purpose": 0,
141                        "securityLevel": 0,
142                        "contractBounds": serde_json::Value::Null,
143                        "type": 0,
144                        "readOnly": false,
145                        "data": "oKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg",
146                    },
147                    {
148                        "$formatVersion": "0",
149                        "id": 1,
150                        "purpose": 0,
151                        "securityLevel": 0,
152                        "contractBounds": serde_json::Value::Null,
153                        "type": 0,
154                        "readOnly": false,
155                        "data": "sbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx",
156                    },
157                ],
158                "balance": 1_000_000u64,
159                "revision": 7,
160            })
161        );
162        let recovered = Identity::from_json(json).expect("from_json");
163        assert_eq!(original, recovered);
164    }
165
166    #[test]
167    fn value_round_trip_with_full_wire_shape() {
168        use crate::serialization::ValueConvertible;
169        let original = fixture();
170        let value = original.to_object().expect("to_object");
171        // Explicit `u32` / `u8` / `u64` suffixes lock typed-int variants.
172        // `Identifier` interpolates as `Value::Identifier`; `BinaryData` of
173        // length 33 lacks a fixed-sized variant, so it stays as
174        // `Value::Bytes(Vec<u8>)`.
175        let id = Identifier::new([0x42; 32]);
176        assert_eq!(
177            value,
178            platform_value!({
179                "$formatVersion": "0",
180                "id": id,
181                // `disabledAt: None` is now stripped per the
182                // `skip_serializing_if` attribute (Phase D step 4).
183                "publicKeys": [
184                    {
185                        "$formatVersion": "0",
186                        "id": 0u32,
187                        "purpose": 0u8,
188                        "securityLevel": 0u8,
189                        "contractBounds": Value::Null,
190                        "type": 0u8,
191                        "readOnly": false,
192                        "data": Value::Bytes(vec![0xa0; 33]),
193                    },
194                    {
195                        "$formatVersion": "0",
196                        "id": 1u32,
197                        "purpose": 0u8,
198                        "securityLevel": 0u8,
199                        "contractBounds": Value::Null,
200                        "type": 0u8,
201                        "readOnly": false,
202                        "data": Value::Bytes(vec![0xb1; 33]),
203                    },
204                ],
205                "balance": 1_000_000u64,
206                "revision": 7u64,
207            })
208        );
209        let recovered = Identity::from_object(value).expect("from_object");
210        assert_eq!(original, recovered);
211    }
212}
213
214/// An identity struct that represent partially set/loaded identity data.
215#[cfg_attr(feature = "json-conversion", json_safe_fields)]
216#[derive(Debug, Clone, Eq, PartialEq)]
217#[cfg_attr(
218    feature = "serde-conversion",
219    derive(serde::Serialize, serde::Deserialize),
220    serde(rename_all = "camelCase")
221)]
222pub struct PartialIdentity {
223    pub id: Identifier,
224    pub loaded_public_keys: BTreeMap<KeyID, IdentityPublicKey>,
225    pub balance: Option<Credits>,
226    pub revision: Option<Revision>,
227    /// These are keys that were requested but didn't exist
228    pub not_found_public_keys: BTreeSet<KeyID>,
229}
230
231impl Identity {
232    #[cfg(feature = "identity-hashing")]
233    /// Computes the hash of an identity
234    pub fn hash(&self) -> Result<Vec<u8>, ProtocolError> {
235        Ok(hash::hash_double_to_vec(
236            PlatformSerializable::serialize_to_bytes(self)?,
237        ))
238    }
239
240    pub fn default_versioned(
241        platform_version: &PlatformVersion,
242    ) -> Result<Identity, ProtocolError> {
243        match platform_version
244            .dpp
245            .identity_versions
246            .identity_structure_version
247        {
248            0 => Ok(Identity::V0(IdentityV0::default())),
249            version => Err(ProtocolError::UnknownVersionMismatch {
250                method: "Identity::default_versioned".to_string(),
251                known_versions: vec![0],
252                received: version,
253            }),
254        }
255    }
256
257    /// Created a new identity based on asset locks and keys
258    pub fn new_with_id_and_keys(
259        id: Identifier,
260        public_keys: BTreeMap<KeyID, IdentityPublicKey>,
261        platform_version: &PlatformVersion,
262    ) -> Result<Identity, ProtocolError> {
263        match platform_version
264            .dpp
265            .identity_versions
266            .identity_structure_version
267        {
268            0 => {
269                let identity_v0 = IdentityV0 {
270                    id,
271                    public_keys,
272                    balance: 0,
273                    revision: 0,
274                };
275                Ok(identity_v0.into())
276            }
277            version => Err(ProtocolError::UnknownVersionMismatch {
278                method: "Identity::new_with_id_and_keys".to_string(),
279                known_versions: vec![0],
280                received: version,
281            }),
282        }
283    }
284
285    /// Create a new identity using input [PlatformAddress]es.
286    ///
287    /// This function derives the identity ID from the provided input addresses.
288    ///
289    /// ## Arguments
290    ///
291    /// * `inputs` - A map of `PlatformAddress` to `(AddressNonce, Credits)`.
292    ///   The identity id is derived from the addresses and nonces (credits are ignored for the id derivation).
293    ///   The nonces should represent state after creation of the identity (e.g. be incremented by 1).
294    /// * `public_keys` - A map of KeyID to IdentityPublicKey tuples representing the public keys for the identity.
295    /// * `platform_version` - The platform version to use for identity creation.
296    ///
297    /// ## Returns
298    ///
299    /// * `Result<Identity, ProtocolError>` - Returns the newly created Identity or a ProtocolError if the operation fails.
300    #[cfg(feature = "state-transitions")]
301    pub fn new_with_input_addresses_and_keys(
302        inputs: &BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
303        public_keys: BTreeMap<KeyID, IdentityPublicKey>,
304        platform_version: &PlatformVersion,
305    ) -> Result<Identity, ProtocolError> {
306        use crate::state_transition::identity_id_from_input_addresses;
307
308        let identity_id = identity_id_from_input_addresses(inputs)?;
309        Self::new_with_id_and_keys(identity_id, public_keys, platform_version)
310    }
311
312    /// Convenience method to get Partial Identity Info
313    pub fn into_partial_identity_info(self) -> PartialIdentity {
314        match self {
315            Identity::V0(v0) => v0.into_partial_identity_info(),
316        }
317    }
318
319    /// Convenience method to get Partial Identity Info
320    pub fn into_partial_identity_info_no_balance(self) -> PartialIdentity {
321        match self {
322            Identity::V0(v0) => v0.into_partial_identity_info_no_balance(),
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::identity::accessors::IdentityGettersV0;
331    use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
332    use crate::identity::{KeyType, Purpose, SecurityLevel};
333    use platform_value::{BinaryData, Identifier};
334    use platform_version::version::LATEST_PLATFORM_VERSION;
335    use std::collections::BTreeMap;
336
337    fn sample_key(id: u32) -> IdentityPublicKey {
338        IdentityPublicKey::V0(IdentityPublicKeyV0 {
339            id,
340            purpose: Purpose::AUTHENTICATION,
341            security_level: SecurityLevel::MASTER,
342            contract_bounds: None,
343            key_type: KeyType::ECDSA_SECP256K1,
344            read_only: false,
345            data: BinaryData::new(vec![0x42; 33]),
346            disabled_at: None,
347        })
348    }
349
350    #[test]
351    fn default_versioned_returns_default_v0() {
352        let identity =
353            Identity::default_versioned(LATEST_PLATFORM_VERSION).expect("default should succeed");
354        assert_eq!(identity.id(), Identifier::default());
355        assert_eq!(identity.balance(), 0);
356        assert_eq!(identity.revision(), 0);
357        assert!(identity.public_keys().is_empty());
358    }
359
360    #[test]
361    fn new_with_id_and_keys_preserves_inputs() {
362        let id = Identifier::from([4u8; 32]);
363        let mut keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
364        keys.insert(0, sample_key(0));
365        keys.insert(1, sample_key(1));
366
367        let identity = Identity::new_with_id_and_keys(id, keys.clone(), LATEST_PLATFORM_VERSION)
368            .expect("new_with_id_and_keys");
369        assert_eq!(identity.id(), id);
370        assert_eq!(identity.balance(), 0);
371        assert_eq!(identity.revision(), 0);
372        assert_eq!(identity.public_keys().len(), 2);
373    }
374
375    #[test]
376    fn into_partial_identity_info_preserves_balance_and_revision() {
377        let mut keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
378        keys.insert(0, sample_key(0));
379        let v0 = IdentityV0 {
380            id: Identifier::from([5u8; 32]),
381            public_keys: keys,
382            balance: 123,
383            revision: 7,
384        };
385        let identity: Identity = v0.clone().into();
386        let partial = identity.into_partial_identity_info();
387        assert_eq!(partial.id, v0.id);
388        assert_eq!(partial.balance, Some(123));
389        assert_eq!(partial.revision, Some(7));
390        assert_eq!(partial.loaded_public_keys.len(), 1);
391        assert!(partial.not_found_public_keys.is_empty());
392    }
393
394    #[test]
395    fn into_partial_identity_info_no_balance_drops_balance() {
396        let v0 = IdentityV0 {
397            id: Identifier::from([6u8; 32]),
398            public_keys: BTreeMap::new(),
399            balance: 999,
400            revision: 2,
401        };
402        let identity: Identity = v0.into();
403        let partial = identity.into_partial_identity_info_no_balance();
404        assert!(partial.balance.is_none());
405        assert_eq!(partial.revision, Some(2));
406    }
407
408    #[test]
409    fn from_v0_conversion_works() {
410        let v0 = IdentityV0 {
411            id: Identifier::from([1u8; 32]),
412            public_keys: BTreeMap::new(),
413            balance: 1,
414            revision: 1,
415        };
416        let identity: Identity = v0.clone().into();
417        match identity {
418            Identity::V0(inner) => assert_eq!(inner, v0),
419        }
420    }
421
422    #[test]
423    fn clone_and_equality() {
424        let id = Identifier::from([3u8; 32]);
425        let identity =
426            Identity::new_with_id_and_keys(id, BTreeMap::new(), LATEST_PLATFORM_VERSION).unwrap();
427        let clone = identity.clone();
428        assert_eq!(identity, clone);
429    }
430
431    #[cfg(feature = "identity-hashing")]
432    #[test]
433    fn hash_is_stable_for_same_identity() {
434        let id = Identifier::from([8u8; 32]);
435        let identity =
436            Identity::new_with_id_and_keys(id, BTreeMap::new(), LATEST_PLATFORM_VERSION).unwrap();
437        let h1 = identity.hash().unwrap();
438        let h2 = identity.hash().unwrap();
439        assert_eq!(h1, h2);
440        // The hash is a fixed-size SHA256-double, 32 bytes.
441        assert_eq!(h1.len(), 32);
442    }
443
444    #[cfg(feature = "identity-hashing")]
445    #[test]
446    fn hash_differs_for_different_identities() {
447        let a = Identity::new_with_id_and_keys(
448            Identifier::from([0u8; 32]),
449            BTreeMap::new(),
450            LATEST_PLATFORM_VERSION,
451        )
452        .unwrap();
453        let b = Identity::new_with_id_and_keys(
454            Identifier::from([1u8; 32]),
455            BTreeMap::new(),
456            LATEST_PLATFORM_VERSION,
457        )
458        .unwrap();
459        assert_ne!(a.hash().unwrap(), b.hash().unwrap());
460    }
461
462    #[cfg(feature = "state-transitions")]
463    #[test]
464    fn new_with_input_addresses_and_keys_is_deterministic() {
465        use crate::address_funds::PlatformAddress;
466
467        let mut inputs: BTreeMap<PlatformAddress, (u32, u64)> = BTreeMap::new();
468        inputs.insert(PlatformAddress::P2pkh([0x11; 20]), (1, 0));
469        inputs.insert(PlatformAddress::P2pkh([0x22; 20]), (2, 0));
470
471        let keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
472
473        let a = Identity::new_with_input_addresses_and_keys(
474            &inputs,
475            keys.clone(),
476            LATEST_PLATFORM_VERSION,
477        )
478        .unwrap();
479        let b = Identity::new_with_input_addresses_and_keys(
480            &inputs,
481            keys.clone(),
482            LATEST_PLATFORM_VERSION,
483        )
484        .unwrap();
485        // Deterministic derivation: same inputs -> same id.
486        assert_eq!(a.id(), b.id());
487    }
488
489    #[cfg(feature = "state-transitions")]
490    #[test]
491    fn new_with_input_addresses_and_keys_fails_on_empty_inputs() {
492        use crate::address_funds::PlatformAddress;
493        let inputs: BTreeMap<PlatformAddress, (u32, u64)> = BTreeMap::new();
494        let keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
495
496        let result =
497            Identity::new_with_input_addresses_and_keys(&inputs, keys, LATEST_PLATFORM_VERSION);
498        assert!(result.is_err());
499    }
500}