casper_types/account/
associated_keys.rs

1//! This module contains types and functions for working with keys associated with an account.
2
3use alloc::{
4    collections::{btree_map::Entry, BTreeMap, BTreeSet},
5    vec::Vec,
6};
7use core::{
8    fmt,
9    fmt::{Display, Formatter},
10};
11
12#[cfg(feature = "datasize")]
13use datasize::DataSize;
14#[cfg(feature = "json-schema")]
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17#[cfg(feature = "json-schema")]
18use serde_map_to_array::KeyValueJsonSchema;
19use serde_map_to_array::{BTreeMapToArray, KeyValueLabels};
20
21use crate::{
22    account::{AccountHash, TryFromIntError, Weight},
23    bytesrepr::{self, FromBytes, ToBytes},
24};
25
26/// Errors that can occur while adding a new [`AccountHash`] to an account's associated keys map.
27#[derive(PartialEq, Eq, Debug, Copy, Clone)]
28#[repr(i32)]
29#[non_exhaustive]
30pub enum AddKeyFailure {
31    /// There are already maximum [`AccountHash`]s associated with the given account.
32    MaxKeysLimit = 1,
33    /// The given [`AccountHash`] is already associated with the given account.
34    DuplicateKey = 2,
35    /// Caller doesn't have sufficient permissions to associate a new [`AccountHash`] with the
36    /// given account.
37    PermissionDenied = 3,
38}
39
40impl Display for AddKeyFailure {
41    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
42        match self {
43            AddKeyFailure::MaxKeysLimit => formatter.write_str(
44                "Unable to add new associated key because maximum amount of keys is reached",
45            ),
46            AddKeyFailure::DuplicateKey => formatter
47                .write_str("Unable to add new associated key because given key already exists"),
48            AddKeyFailure::PermissionDenied => formatter
49                .write_str("Unable to add new associated key due to insufficient permissions"),
50        }
51    }
52}
53
54// This conversion is not intended to be used by third party crates.
55#[doc(hidden)]
56impl TryFrom<i32> for AddKeyFailure {
57    type Error = TryFromIntError;
58
59    fn try_from(value: i32) -> Result<Self, Self::Error> {
60        match value {
61            d if d == AddKeyFailure::MaxKeysLimit as i32 => Ok(AddKeyFailure::MaxKeysLimit),
62            d if d == AddKeyFailure::DuplicateKey as i32 => Ok(AddKeyFailure::DuplicateKey),
63            d if d == AddKeyFailure::PermissionDenied as i32 => Ok(AddKeyFailure::PermissionDenied),
64            _ => Err(TryFromIntError(())),
65        }
66    }
67}
68
69/// Errors that can occur while removing a [`AccountHash`] from an account's associated keys map.
70#[derive(Debug, Eq, PartialEq, Copy, Clone)]
71#[repr(i32)]
72#[non_exhaustive]
73pub enum RemoveKeyFailure {
74    /// The given [`AccountHash`] is not associated with the given account.
75    MissingKey = 1,
76    /// Caller doesn't have sufficient permissions to remove an associated [`AccountHash`] from the
77    /// given account.
78    PermissionDenied = 2,
79    /// Removing the given associated [`AccountHash`] would cause the total weight of all remaining
80    /// `AccountHash`s to fall below one of the action thresholds for the given account.
81    ThresholdViolation = 3,
82}
83
84impl Display for RemoveKeyFailure {
85    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
86        match self {
87            RemoveKeyFailure::MissingKey => {
88                formatter.write_str("Unable to remove a key that does not exist")
89            }
90            RemoveKeyFailure::PermissionDenied => formatter
91                .write_str("Unable to remove associated key due to insufficient permissions"),
92            RemoveKeyFailure::ThresholdViolation => formatter.write_str(
93                "Unable to remove a key which would violate action threshold constraints",
94            ),
95        }
96    }
97}
98
99// This conversion is not intended to be used by third party crates.
100#[doc(hidden)]
101impl TryFrom<i32> for RemoveKeyFailure {
102    type Error = TryFromIntError;
103
104    fn try_from(value: i32) -> Result<Self, Self::Error> {
105        match value {
106            d if d == RemoveKeyFailure::MissingKey as i32 => Ok(RemoveKeyFailure::MissingKey),
107            d if d == RemoveKeyFailure::PermissionDenied as i32 => {
108                Ok(RemoveKeyFailure::PermissionDenied)
109            }
110            d if d == RemoveKeyFailure::ThresholdViolation as i32 => {
111                Ok(RemoveKeyFailure::ThresholdViolation)
112            }
113            _ => Err(TryFromIntError(())),
114        }
115    }
116}
117
118/// Errors that can occur while updating the [`crate::addressable_entity::Weight`] of a
119/// [`AccountHash`] in an account's associated keys map.
120#[derive(PartialEq, Eq, Debug, Copy, Clone)]
121#[repr(i32)]
122#[non_exhaustive]
123pub enum UpdateKeyFailure {
124    /// The given [`AccountHash`] is not associated with the given account.
125    MissingKey = 1,
126    /// Caller doesn't have sufficient permissions to update an associated [`AccountHash`] from the
127    /// given account.
128    PermissionDenied = 2,
129    /// Updating the [`crate::addressable_entity::Weight`] of the given associated [`AccountHash`]
130    /// would cause the total weight of all `AccountHash`s to fall below one of the action
131    /// thresholds for the given account.
132    ThresholdViolation = 3,
133}
134
135impl Display for UpdateKeyFailure {
136    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
137        match self {
138            UpdateKeyFailure::MissingKey => formatter.write_str(
139                "Unable to update the value under an associated key that does not exist",
140            ),
141            UpdateKeyFailure::PermissionDenied => formatter
142                .write_str("Unable to update associated key due to insufficient permissions"),
143            UpdateKeyFailure::ThresholdViolation => formatter.write_str(
144                "Unable to update weight that would fall below any of action thresholds",
145            ),
146        }
147    }
148}
149
150// This conversion is not intended to be used by third party crates.
151#[doc(hidden)]
152impl TryFrom<i32> for UpdateKeyFailure {
153    type Error = TryFromIntError;
154
155    fn try_from(value: i32) -> Result<Self, Self::Error> {
156        match value {
157            d if d == UpdateKeyFailure::MissingKey as i32 => Ok(UpdateKeyFailure::MissingKey),
158            d if d == UpdateKeyFailure::PermissionDenied as i32 => {
159                Ok(UpdateKeyFailure::PermissionDenied)
160            }
161            d if d == UpdateKeyFailure::ThresholdViolation as i32 => {
162                Ok(UpdateKeyFailure::ThresholdViolation)
163            }
164            _ => Err(TryFromIntError(())),
165        }
166    }
167}
168
169/// A collection of weighted public keys (represented as account hashes) associated with an account.
170#[derive(Default, PartialOrd, Ord, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
171#[cfg_attr(feature = "datasize", derive(DataSize))]
172#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
173#[cfg_attr(feature = "json-schema", schemars(rename = "AccountAssociatedKeys"))]
174#[serde(deny_unknown_fields)]
175#[rustfmt::skip]
176pub struct AssociatedKeys(
177    #[serde(with = "BTreeMapToArray::<AccountHash, Weight, Labels>")]
178    BTreeMap<AccountHash, Weight>,
179);
180
181impl AssociatedKeys {
182    /// Constructs a new AssociatedKeys.
183    pub fn new(key: AccountHash, weight: Weight) -> AssociatedKeys {
184        let mut bt: BTreeMap<AccountHash, Weight> = BTreeMap::new();
185        bt.insert(key, weight);
186        AssociatedKeys(bt)
187    }
188
189    /// Adds a new AssociatedKey to the set.
190    ///
191    /// Returns true if added successfully, false otherwise.
192    pub fn add_key(&mut self, key: AccountHash, weight: Weight) -> Result<(), AddKeyFailure> {
193        match self.0.entry(key) {
194            Entry::Vacant(entry) => {
195                entry.insert(weight);
196            }
197            Entry::Occupied(_) => return Err(AddKeyFailure::DuplicateKey),
198        }
199        Ok(())
200    }
201
202    /// Removes key from the associated keys set.
203    /// Returns true if value was found in the set prior to the removal, false
204    /// otherwise.
205    pub fn remove_key(&mut self, key: &AccountHash) -> Result<(), RemoveKeyFailure> {
206        self.0
207            .remove(key)
208            .map(|_| ())
209            .ok_or(RemoveKeyFailure::MissingKey)
210    }
211
212    /// Adds new AssociatedKey to the set.
213    /// Returns true if added successfully, false otherwise.
214    pub fn update_key(&mut self, key: AccountHash, weight: Weight) -> Result<(), UpdateKeyFailure> {
215        match self.0.entry(key) {
216            Entry::Vacant(_) => {
217                return Err(UpdateKeyFailure::MissingKey);
218            }
219            Entry::Occupied(mut entry) => {
220                *entry.get_mut() = weight;
221            }
222        }
223        Ok(())
224    }
225
226    /// Returns the weight of an account hash.
227    pub fn get(&self, key: &AccountHash) -> Option<&Weight> {
228        self.0.get(key)
229    }
230
231    /// Returns `true` if a given key exists.
232    pub fn contains_key(&self, key: &AccountHash) -> bool {
233        self.0.contains_key(key)
234    }
235
236    /// Returns an iterator over the account hash and the weights.
237    pub fn iter(&self) -> impl Iterator<Item = (&AccountHash, &Weight)> {
238        self.0.iter()
239    }
240
241    /// Returns the count of the associated keys.
242    pub fn len(&self) -> usize {
243        self.0.len()
244    }
245
246    /// Returns `true` if the associated keys are empty.
247    pub fn is_empty(&self) -> bool {
248        self.0.is_empty()
249    }
250
251    /// Helper method that calculates weight for keys that comes from any
252    /// source.
253    ///
254    /// This method is not concerned about uniqueness of the passed iterable.
255    /// Uniqueness is determined based on the input collection properties,
256    /// which is either BTreeSet (in [`AssociatedKeys::calculate_keys_weight`])
257    /// or BTreeMap (in [`AssociatedKeys::total_keys_weight`]).
258    fn calculate_any_keys_weight<'a>(&self, keys: impl Iterator<Item = &'a AccountHash>) -> Weight {
259        let total = keys
260            .filter_map(|key| self.0.get(key))
261            .fold(0u8, |acc, w| acc.saturating_add(w.value()));
262
263        Weight::new(total)
264    }
265
266    /// Calculates total weight of authorization keys provided by an argument
267    pub fn calculate_keys_weight(&self, authorization_keys: &BTreeSet<AccountHash>) -> Weight {
268        self.calculate_any_keys_weight(authorization_keys.iter())
269    }
270
271    /// Calculates total weight of all authorization keys
272    pub fn total_keys_weight(&self) -> Weight {
273        self.calculate_any_keys_weight(self.0.keys())
274    }
275
276    /// Calculates total weight of all authorization keys excluding a given key
277    pub fn total_keys_weight_excluding(&self, account_hash: AccountHash) -> Weight {
278        self.calculate_any_keys_weight(self.0.keys().filter(|&&element| element != account_hash))
279    }
280}
281
282impl From<BTreeMap<AccountHash, Weight>> for AssociatedKeys {
283    fn from(associated_keys: BTreeMap<AccountHash, Weight>) -> Self {
284        Self(associated_keys)
285    }
286}
287
288impl From<AssociatedKeys> for BTreeMap<AccountHash, Weight> {
289    fn from(associated_keys: AssociatedKeys) -> Self {
290        associated_keys.0
291    }
292}
293
294impl ToBytes for AssociatedKeys {
295    fn to_bytes(&self) -> Result<Vec<u8>, bytesrepr::Error> {
296        self.0.to_bytes()
297    }
298
299    fn serialized_length(&self) -> usize {
300        self.0.serialized_length()
301    }
302
303    fn write_bytes(&self, writer: &mut Vec<u8>) -> Result<(), bytesrepr::Error> {
304        self.0.write_bytes(writer)
305    }
306}
307
308impl FromBytes for AssociatedKeys {
309    fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> {
310        let (associated_keys, rem) = FromBytes::from_bytes(bytes)?;
311        Ok((AssociatedKeys(associated_keys), rem))
312    }
313}
314
315struct Labels;
316
317impl KeyValueLabels for Labels {
318    const KEY: &'static str = "account_hash";
319    const VALUE: &'static str = "weight";
320}
321
322#[cfg(feature = "json-schema")]
323impl KeyValueJsonSchema for Labels {
324    const JSON_SCHEMA_KV_NAME: Option<&'static str> = Some("AssociatedKey");
325    const JSON_SCHEMA_KV_DESCRIPTION: Option<&'static str> = Some("A weighted public key.");
326    const JSON_SCHEMA_KEY_DESCRIPTION: Option<&'static str> =
327        Some("The account hash of the public key.");
328    const JSON_SCHEMA_VALUE_DESCRIPTION: Option<&'static str> =
329        Some("The weight assigned to the public key.");
330}
331
332#[doc(hidden)]
333#[cfg(any(feature = "testing", feature = "gens", test))]
334pub mod gens {
335    use proptest::prelude::*;
336
337    use crate::gens::{account_hash_arb, account_weight_arb};
338
339    use super::AssociatedKeys;
340
341    pub fn account_associated_keys_arb() -> impl Strategy<Value = AssociatedKeys> {
342        proptest::collection::btree_map(account_hash_arb(), account_weight_arb(), 10).prop_map(
343            |keys| {
344                let mut associated_keys = AssociatedKeys::default();
345                keys.into_iter().for_each(|(k, v)| {
346                    associated_keys.add_key(k, v).unwrap();
347                });
348                associated_keys
349            },
350        )
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use std::{collections::BTreeSet, iter::FromIterator};
357
358    use crate::{
359        account::{AccountHash, Weight, ACCOUNT_HASH_LENGTH},
360        bytesrepr,
361    };
362
363    use super::*;
364
365    #[test]
366    fn associated_keys_add() {
367        let mut keys =
368            AssociatedKeys::new(AccountHash::new([0u8; ACCOUNT_HASH_LENGTH]), Weight::new(1));
369        let new_pk = AccountHash::new([1u8; ACCOUNT_HASH_LENGTH]);
370        let new_pk_weight = Weight::new(2);
371        assert!(keys.add_key(new_pk, new_pk_weight).is_ok());
372        assert_eq!(keys.get(&new_pk), Some(&new_pk_weight))
373    }
374
375    #[test]
376    fn associated_keys_add_duplicate() {
377        let pk = AccountHash::new([0u8; ACCOUNT_HASH_LENGTH]);
378        let weight = Weight::new(1);
379        let mut keys = AssociatedKeys::new(pk, weight);
380        assert_eq!(
381            keys.add_key(pk, Weight::new(10)),
382            Err(AddKeyFailure::DuplicateKey)
383        );
384        assert_eq!(keys.get(&pk), Some(&weight));
385    }
386
387    #[test]
388    fn associated_keys_remove() {
389        let pk = AccountHash::new([0u8; ACCOUNT_HASH_LENGTH]);
390        let weight = Weight::new(1);
391        let mut keys = AssociatedKeys::new(pk, weight);
392        assert!(keys.remove_key(&pk).is_ok());
393        assert!(keys
394            .remove_key(&AccountHash::new([1u8; ACCOUNT_HASH_LENGTH]))
395            .is_err());
396    }
397
398    #[test]
399    fn associated_keys_update() {
400        let pk1 = AccountHash::new([0u8; ACCOUNT_HASH_LENGTH]);
401        let pk2 = AccountHash::new([1u8; ACCOUNT_HASH_LENGTH]);
402        let weight = Weight::new(1);
403        let mut keys = AssociatedKeys::new(pk1, weight);
404        assert!(matches!(
405            keys.update_key(pk2, Weight::new(2))
406                .expect_err("should get error"),
407            UpdateKeyFailure::MissingKey
408        ));
409        keys.add_key(pk2, Weight::new(1)).unwrap();
410        assert_eq!(keys.get(&pk2), Some(&Weight::new(1)));
411        keys.update_key(pk2, Weight::new(2)).unwrap();
412        assert_eq!(keys.get(&pk2), Some(&Weight::new(2)));
413    }
414
415    #[test]
416    fn associated_keys_calculate_keys_once() {
417        let key_1 = AccountHash::new([0; 32]);
418        let key_2 = AccountHash::new([1; 32]);
419        let key_3 = AccountHash::new([2; 32]);
420        let mut keys = AssociatedKeys::default();
421
422        keys.add_key(key_2, Weight::new(2))
423            .expect("should add key_1");
424        keys.add_key(key_1, Weight::new(1))
425            .expect("should add key_1");
426        keys.add_key(key_3, Weight::new(3))
427            .expect("should add key_1");
428
429        assert_eq!(
430            keys.calculate_keys_weight(&BTreeSet::from_iter(vec![
431                key_1, key_2, key_3, key_1, key_2, key_3,
432            ])),
433            Weight::new(1 + 2 + 3)
434        );
435    }
436
437    #[test]
438    fn associated_keys_total_weight() {
439        let associated_keys = {
440            let mut res = AssociatedKeys::new(AccountHash::new([1u8; 32]), Weight::new(1));
441            res.add_key(AccountHash::new([2u8; 32]), Weight::new(11))
442                .expect("should add key 1");
443            res.add_key(AccountHash::new([3u8; 32]), Weight::new(12))
444                .expect("should add key 2");
445            res.add_key(AccountHash::new([4u8; 32]), Weight::new(13))
446                .expect("should add key 3");
447            res
448        };
449        assert_eq!(
450            associated_keys.total_keys_weight(),
451            Weight::new(1 + 11 + 12 + 13)
452        );
453    }
454
455    #[test]
456    fn associated_keys_total_weight_excluding() {
457        let identity_key = AccountHash::new([1u8; 32]);
458        let identity_key_weight = Weight::new(1);
459
460        let key_1 = AccountHash::new([2u8; 32]);
461        let key_1_weight = Weight::new(11);
462
463        let key_2 = AccountHash::new([3u8; 32]);
464        let key_2_weight = Weight::new(12);
465
466        let key_3 = AccountHash::new([4u8; 32]);
467        let key_3_weight = Weight::new(13);
468
469        let associated_keys = {
470            let mut res = AssociatedKeys::new(identity_key, identity_key_weight);
471            res.add_key(key_1, key_1_weight).expect("should add key 1");
472            res.add_key(key_2, key_2_weight).expect("should add key 2");
473            res.add_key(key_3, key_3_weight).expect("should add key 3");
474            res
475        };
476        assert_eq!(
477            associated_keys.total_keys_weight_excluding(key_2),
478            Weight::new(identity_key_weight.value() + key_1_weight.value() + key_3_weight.value())
479        );
480    }
481
482    #[test]
483    fn overflowing_keys_weight() {
484        let identity_key = AccountHash::new([1u8; 32]);
485        let key_1 = AccountHash::new([2u8; 32]);
486        let key_2 = AccountHash::new([3u8; 32]);
487        let key_3 = AccountHash::new([4u8; 32]);
488
489        let identity_key_weight = Weight::new(250);
490        let weight_1 = Weight::new(1);
491        let weight_2 = Weight::new(2);
492        let weight_3 = Weight::new(3);
493
494        let saturated_weight = Weight::new(u8::MAX);
495
496        let associated_keys = {
497            let mut res = AssociatedKeys::new(identity_key, identity_key_weight);
498
499            res.add_key(key_1, weight_1).expect("should add key 1");
500            res.add_key(key_2, weight_2).expect("should add key 2");
501            res.add_key(key_3, weight_3).expect("should add key 3");
502            res
503        };
504
505        assert_eq!(
506            associated_keys.calculate_keys_weight(&BTreeSet::from_iter(vec![
507                identity_key, // 250
508                key_1,        // 251
509                key_2,        // 253
510                key_3,        // 256 - error
511            ])),
512            saturated_weight,
513        );
514    }
515
516    #[test]
517    fn serialization_roundtrip() {
518        let mut keys = AssociatedKeys::default();
519        keys.add_key(AccountHash::new([1; 32]), Weight::new(1))
520            .unwrap();
521        keys.add_key(AccountHash::new([2; 32]), Weight::new(2))
522            .unwrap();
523        keys.add_key(AccountHash::new([3; 32]), Weight::new(3))
524            .unwrap();
525        bytesrepr::test_serialization_roundtrip(&keys);
526    }
527}