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