Skip to main content

dpp/identity/
identity.rs

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