Skip to main content

dpp/identity/identity_public_key/v1/
mod.rs

1mod accessors;
2mod methods;
3
4use bincode::{Decode, DecodeUntrusted, Encode};
5
6use crate::fee::Credits;
7use crate::identity::identity_public_key::contract_bounds::ContractBounds;
8use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
9use crate::identity::{KeyID, KeyType, Purpose, SecurityLevel, TimestampMillis};
10#[cfg(feature = "json-conversion")]
11use crate::serialization::json_safe_fields;
12#[cfg(feature = "state-transitions")]
13use crate::state_transition::public_key_in_creation::v1::IdentityPublicKeyInCreationV1;
14use platform_value::BinaryData;
15use serde::{Deserialize, Serialize};
16
17/// An identity public key that may carry usage limits.
18///
19/// The first eight fields are the `IdentityPublicKeyV0` fields in the same order. `total_budget` and
20/// `expires_at` exist from protocol version 14 and are only allowed on AUTHENTICATION keys below
21/// the MASTER security level. A key without limits keeps being written as V0.
22#[cfg_attr(feature = "json-conversion", json_safe_fields)]
23#[derive(
24    Default,
25    Debug,
26    Serialize,
27    Deserialize,
28    Encode,
29    Decode,
30    Clone,
31    PartialEq,
32    Eq,
33    Ord,
34    PartialOrd,
35    Hash,
36    DecodeUntrusted,
37)]
38#[serde(rename_all = "camelCase")]
39pub struct IdentityPublicKeyV1 {
40    pub id: KeyID,
41    pub purpose: Purpose,
42    pub security_level: SecurityLevel,
43    pub contract_bounds: Option<ContractBounds>,
44    #[serde(rename = "type")]
45    pub key_type: KeyType,
46    pub read_only: bool,
47    pub data: BinaryData,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub disabled_at: Option<TimestampMillis>,
50    /// The total credits that state transitions signed with this key may take from the
51    /// identity over its lifetime. It never changes: how much of it is left lives in Drive,
52    /// not in the key.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub total_budget: Option<Credits>,
55    /// The block time, in milliseconds, from which the key can no longer sign.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub expires_at: Option<TimestampMillis>,
58}
59
60impl IdentityPublicKeyV1 {
61    /// Adds usage limits to a V0 key.
62    pub fn from_v0_with_limits(
63        key: IdentityPublicKeyV0,
64        total_budget: Option<Credits>,
65        expires_at: Option<TimestampMillis>,
66    ) -> Self {
67        let IdentityPublicKeyV0 {
68            id,
69            purpose,
70            security_level,
71            contract_bounds,
72            key_type,
73            read_only,
74            data,
75            disabled_at,
76        } = key;
77        IdentityPublicKeyV1 {
78            id,
79            purpose,
80            security_level,
81            contract_bounds,
82            key_type,
83            read_only,
84            data,
85            disabled_at,
86            total_budget,
87            expires_at,
88        }
89    }
90}
91
92#[cfg(feature = "state-transitions")]
93impl From<&IdentityPublicKeyV1> for IdentityPublicKeyInCreationV1 {
94    fn from(key: &IdentityPublicKeyV1) -> Self {
95        IdentityPublicKeyInCreationV1 {
96            id: key.id,
97            key_type: key.key_type,
98            purpose: key.purpose,
99            security_level: key.security_level,
100            contract_bounds: key.contract_bounds.clone(),
101            read_only: key.read_only,
102            data: key.data.clone(),
103            total_budget: key.total_budget,
104            expires_at: key.expires_at,
105            signature: BinaryData::default(),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
114    use crate::identity::identity_public_key::accessors::v1::IdentityPublicKeyGettersV1;
115    use crate::identity::IdentityPublicKey;
116    use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
117
118    fn key_v0() -> IdentityPublicKeyV0 {
119        IdentityPublicKeyV0 {
120            id: 7,
121            purpose: Purpose::AUTHENTICATION,
122            security_level: SecurityLevel::HIGH,
123            contract_bounds: None,
124            key_type: KeyType::ECDSA_SECP256K1,
125            read_only: false,
126            data: BinaryData::new(vec![3; 33]),
127            disabled_at: None,
128        }
129    }
130
131    #[test]
132    fn should_round_trip_a_limited_key_through_platform_serialization() {
133        let key: IdentityPublicKey =
134            IdentityPublicKeyV1::from_v0_with_limits(key_v0(), Some(5_000_000), Some(1_800_000))
135                .into();
136        let bytes = key.serialize_to_bytes().expect("expected to serialize");
137        let decoded = IdentityPublicKey::deserialize_from_bytes_untrusted(&bytes)
138            .expect("expected to deserialize");
139        assert_eq!(decoded, key);
140        assert_eq!(decoded.total_budget(), Some(5_000_000));
141        assert_eq!(decoded.expires_at(), Some(1_800_000));
142    }
143
144    #[test]
145    fn should_leave_the_version_0_encoding_untouched() {
146        // Every key already in state is a version 0 key, so its bytes must keep decoding to the
147        // same key, and a key without limits must keep encoding to the same bytes.
148        let key: IdentityPublicKey = key_v0().into();
149        let bytes = key.serialize_to_bytes().expect("expected to serialize");
150        assert_eq!(bytes[0], 0, "the variant index of a version 0 key");
151        let decoded = IdentityPublicKey::deserialize_from_bytes_untrusted(&bytes)
152            .expect("expected to deserialize");
153        assert!(matches!(decoded, IdentityPublicKey::V0(_)));
154        assert!(!decoded.has_limits());
155
156        // Version 1 is the version 0 body followed by the two limits.
157        let limited: IdentityPublicKey =
158            IdentityPublicKeyV1::from_v0_with_limits(key_v0(), None, None).into();
159        let limited_bytes = limited.serialize_to_bytes().expect("expected to serialize");
160        assert_eq!(limited_bytes[0], 1);
161        assert_eq!(limited_bytes[1..bytes.len()], bytes[1..]);
162        assert_eq!(limited_bytes[bytes.len()..], [0, 0]);
163    }
164
165    #[test]
166    fn should_upgrade_a_version_0_key_when_limits_are_added() {
167        let key: IdentityPublicKey = key_v0().into();
168        let limited = key.clone().with_limits(Some(10), None);
169        assert!(matches!(limited, IdentityPublicKey::V1(_)));
170        assert_eq!(limited.id(), key.id());
171        assert_eq!(limited.data(), key.data());
172        assert_eq!(limited.total_budget(), Some(10));
173        assert_eq!(limited.expires_at(), None);
174
175        let relimited = limited.with_limits(None, Some(20));
176        assert_eq!(relimited.total_budget(), None);
177        assert_eq!(relimited.expires_at(), Some(20));
178    }
179
180    #[test]
181    fn should_be_expired_from_the_expiry_instant_on() {
182        let key = IdentityPublicKeyV1::from_v0_with_limits(key_v0(), None, Some(1_000));
183        assert!(!key.is_expired_at(999));
184        assert!(key.is_expired_at(1_000));
185        assert!(key.is_expired_at(1_001));
186
187        let never = IdentityPublicKeyV1::from_v0_with_limits(key_v0(), Some(1), None);
188        assert!(!never.is_expired_at(u64::MAX));
189    }
190
191    #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
192    #[test]
193    fn should_tag_the_json_shape_with_format_version_1() {
194        use crate::serialization::JsonConvertible;
195        use serde_json::json;
196
197        let key: IdentityPublicKey =
198            IdentityPublicKeyV1::from_v0_with_limits(key_v0(), Some(5_000), Some(1_800_000)).into();
199        let json = key.to_json().expect("to_json");
200        assert_eq!(json["$formatVersion"], json!("1"));
201        assert_eq!(json["totalBudget"], json!(5_000));
202        assert_eq!(json["expiresAt"], json!(1_800_000));
203        assert_eq!(IdentityPublicKey::from_json(json).expect("from_json"), key);
204
205        // The limits are left out of the JSON shape when absent, like `disabledAt`.
206        let unlimited: IdentityPublicKey =
207            IdentityPublicKeyV1::from_v0_with_limits(key_v0(), None, None).into();
208        let json = unlimited.to_json().expect("to_json");
209        assert!(json.get("totalBudget").is_none());
210        assert!(json.get("expiresAt").is_none());
211        assert_eq!(
212            IdentityPublicKey::from_json(json).expect("from_json"),
213            unlimited
214        );
215    }
216}