Skip to main content

dpp/contract_group/
mod.rs

1//! Contract groups.
2//!
3//! A contract group is an identity-owned set of contracts, contract document types and contract
4//! tokens. A group is registered by a data contract create transition, which derives the group id
5//! from the registering identity and the transition's identity nonce. Later contracts created by
6//! an owner of the group can add themselves, one of their document types, or one of their tokens
7//! to the group in their own create transition.
8//!
9//! Drive stores every group under the `ContractGroups` root tree together with a backwards index
10//! from each member contract to the groups it belongs to.
11
12use crate::data_contract::{DocumentName, TokenContractPosition};
13use crate::prelude::IdentityNonce;
14use crate::util::hash::hash_double;
15use crate::ProtocolError;
16use bincode::{Decode, DecodeUntrusted, Encode};
17use derive_more::From;
18use platform_serialization_derive::{
19    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
20};
21use platform_value::Identifier;
22#[cfg(feature = "serde-conversion")]
23use serde::{Deserialize, Serialize};
24use std::collections::BTreeSet;
25use std::fmt;
26
27/// The domain prefix hashed into every contract group id.
28pub const CONTRACT_GROUP_ID_DOMAIN: &[u8] = b"contract_group";
29
30/// Derives the id of the contract group registered by a data contract create transition.
31///
32/// The id is `hash_double("contract_group" || owner id || identity nonce (big endian))`. The
33/// contract created by the same transition has id `hash_double(owner id || identity nonce)`, so
34/// both ids are known to the client before broadcasting and can never collide.
35pub fn generate_contract_group_id(
36    owner_id: &Identifier,
37    identity_nonce: IdentityNonce,
38) -> Identifier {
39    let mut bytes = CONTRACT_GROUP_ID_DOMAIN.to_vec();
40    bytes.extend_from_slice(owner_id.as_slice());
41    bytes.extend_from_slice(&identity_nonce.to_be_bytes());
42    Identifier::from(hash_double(bytes))
43}
44
45/// Who owns a contract group, and who may add members to it.
46///
47/// Every group has exactly one owner: the identity that registered it. A member is added by the
48/// identity that creates the member contract, so a join is allowed when that identity is the
49/// owner or one of the group's admins. Admins act alone; there is no threshold.
50#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
51#[cfg_attr(
52    feature = "serde-conversion",
53    derive(Serialize, Deserialize),
54    serde(rename_all = "camelCase")
55)]
56pub enum ContractGroupOwner {
57    /// One identity owns the group and is the only one who may add members.
58    SingleOwner(Identifier),
59    /// One identity owns the group and a set of admins may add members alongside it. At least
60    /// one admin, at most `SystemLimits::max_contract_group_admins`, none of them the owner.
61    OwnerAndAdmins {
62        /// The owner: the identity that registered the group.
63        owner: Identifier,
64        /// The identities that may add members besides the owner.
65        admins: BTreeSet<Identifier>,
66    },
67}
68
69impl ContractGroupOwner {
70    /// The identity that owns the group.
71    pub fn owner_id(&self) -> &Identifier {
72        match self {
73            ContractGroupOwner::SingleOwner(owner_id) => owner_id,
74            ContractGroupOwner::OwnerAndAdmins { owner, .. } => owner,
75        }
76    }
77
78    /// The identities that may add members besides the owner. Empty for a single owner.
79    pub fn admin_ids(&self) -> Option<&BTreeSet<Identifier>> {
80        match self {
81            ContractGroupOwner::SingleOwner(_) => None,
82            ContractGroupOwner::OwnerAndAdmins { admins, .. } => Some(admins),
83        }
84    }
85
86    /// The number of admins.
87    pub fn admin_count(&self) -> usize {
88        self.admin_ids().map_or(0, BTreeSet::len)
89    }
90
91    /// Whether `identity_id` may add members to the group: the owner or an admin.
92    pub fn may_add_members(&self, identity_id: &Identifier) -> bool {
93        self.owner_id() == identity_id
94            || self
95                .admin_ids()
96                .is_some_and(|admins| admins.contains(identity_id))
97    }
98}
99
100impl fmt::Display for ContractGroupOwner {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            ContractGroupOwner::SingleOwner(owner_id) => write!(f, "single owner {}", owner_id),
104            ContractGroupOwner::OwnerAndAdmins { owner, admins } => {
105                write!(f, "owner {} with {} admins", owner, admins.len())
106            }
107        }
108    }
109}
110
111/// What part of the contract being created joins a contract group.
112///
113/// A member always belongs to the contract of the create transition that declares it; a contract
114/// cannot enrol another contract.
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, DecodeUntrusted)]
116#[cfg_attr(
117    feature = "serde-conversion",
118    derive(Serialize, Deserialize),
119    serde(rename_all = "camelCase")
120)]
121pub enum ContractGroupMember {
122    /// The whole contract: every document type and every token it has or will have.
123    Contract,
124    /// One document type of the contract.
125    DocumentType(DocumentName),
126    /// One token of the contract, by its position in the contract.
127    Token(TokenContractPosition),
128}
129
130impl fmt::Display for ContractGroupMember {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            ContractGroupMember::Contract => write!(f, "contract"),
134            ContractGroupMember::DocumentType(name) => write!(f, "document type {}", name),
135            ContractGroupMember::Token(position) => write!(f, "token {}", position),
136        }
137    }
138}
139
140/// A declaration that a part of the created contract joins a contract group.
141#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, DecodeUntrusted)]
142#[cfg_attr(
143    feature = "serde-conversion",
144    derive(Serialize, Deserialize),
145    serde(rename_all = "camelCase")
146)]
147pub struct ContractGroupMembership {
148    /// The group joined. Either a group registered earlier by an owner, or the group registered
149    /// by this same create transition.
150    pub contract_group_id: Identifier,
151    /// The part of the created contract that joins.
152    pub member: ContractGroupMember,
153}
154
155/// The registration of a new contract group, carried by a data contract create transition.
156///
157/// The owner is the identity that signs the transition and is not on the wire; the group id is
158/// derived from it and the identity nonce with [`generate_contract_group_id`].
159#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
160#[cfg_attr(
161    feature = "serde-conversion",
162    derive(Serialize, Deserialize),
163    serde(rename_all = "camelCase")
164)]
165pub struct ContractGroupRegistration {
166    /// The identities that may add members alongside the owner. Empty for a group with a single
167    /// owner; otherwise at most `SystemLimits::max_contract_group_admins` distinct identities,
168    /// none of them the owner.
169    #[cfg_attr(feature = "serde-conversion", serde(default))]
170    pub admins: BTreeSet<Identifier>,
171    /// An optional human readable name, bounded by `SystemLimits::max_contract_group_name_length`.
172    pub name: Option<String>,
173    /// An optional description, bounded by
174    /// `SystemLimits::max_contract_group_description_length`.
175    pub description: Option<String>,
176}
177
178/// The stored information of a contract group, kept as one item under the group's tree.
179#[derive(
180    Debug,
181    Clone,
182    PartialEq,
183    Eq,
184    Encode,
185    Decode,
186    DecodeUntrusted,
187    PlatformSerialize,
188    PlatformDeserializeTrusted,
189    PlatformDeserializeUntrusted,
190    From,
191)]
192#[cfg_attr(
193    feature = "serde-conversion",
194    derive(Serialize, Deserialize),
195    serde(tag = "$formatVersion")
196)]
197#[platform_serialize(unversioned)]
198pub enum ContractGroupInfo {
199    /// Version 0.
200    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
201    V0(ContractGroupInfoV0),
202}
203
204/// Version 0 of the stored contract group information.
205#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
206#[cfg_attr(
207    feature = "serde-conversion",
208    derive(Serialize, Deserialize),
209    serde(rename_all = "camelCase")
210)]
211pub struct ContractGroupInfoV0 {
212    /// The owner of the group, alone or with admins.
213    pub owner: ContractGroupOwner,
214    /// An optional human readable name.
215    pub name: Option<String>,
216    /// An optional description.
217    pub description: Option<String>,
218}
219
220impl ContractGroupInfo {
221    /// The owner of the group, alone or with admins.
222    pub fn owner(&self) -> &ContractGroupOwner {
223        match self {
224            ContractGroupInfo::V0(info) => &info.owner,
225        }
226    }
227
228    /// The group's name, when it has one.
229    pub fn name(&self) -> Option<&str> {
230        match self {
231            ContractGroupInfo::V0(info) => info.name.as_deref(),
232        }
233    }
234
235    /// The group's description, when it has one.
236    pub fn description(&self) -> Option<&str> {
237        match self {
238            ContractGroupInfo::V0(info) => info.description.as_deref(),
239        }
240    }
241}
242
243impl From<(Identifier, ContractGroupRegistration)> for ContractGroupInfo {
244    /// Builds the stored information of a group registered by `owner_id`.
245    fn from((owner_id, registration): (Identifier, ContractGroupRegistration)) -> Self {
246        let ContractGroupRegistration {
247            admins,
248            name,
249            description,
250        } = registration;
251        let owner = if admins.is_empty() {
252            ContractGroupOwner::SingleOwner(owner_id)
253        } else {
254            ContractGroupOwner::OwnerAndAdmins {
255                owner: owner_id,
256                admins,
257            }
258        };
259        ContractGroupInfo::V0(ContractGroupInfoV0 {
260            owner,
261            name,
262            description,
263        })
264    }
265}
266
267impl From<(Identifier, &ContractGroupRegistration)> for ContractGroupInfo {
268    fn from((owner_id, registration): (Identifier, &ContractGroupRegistration)) -> Self {
269        (owner_id, registration.clone()).into()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::data_contract::DataContract;
277
278    #[test]
279    fn should_derive_a_contract_group_id_distinct_from_the_contract_id() {
280        let owner_id = Identifier::from([7u8; 32]);
281        let nonce = 42;
282
283        let group_id = generate_contract_group_id(&owner_id, nonce);
284        let contract_id = DataContract::generate_data_contract_id_v0(owner_id, nonce);
285
286        assert_ne!(group_id, contract_id);
287        assert_eq!(group_id, generate_contract_group_id(&owner_id, nonce));
288        assert_ne!(group_id, generate_contract_group_id(&owner_id, nonce + 1));
289        assert_ne!(
290            group_id,
291            generate_contract_group_id(&Identifier::from([8u8; 32]), nonce)
292        );
293    }
294
295    #[test]
296    fn should_resolve_who_may_add_members_for_both_owner_kinds() {
297        let alice = Identifier::from([1u8; 32]);
298        let bob = Identifier::from([2u8; 32]);
299        let carol = Identifier::from([3u8; 32]);
300
301        let single = ContractGroupOwner::SingleOwner(alice);
302        assert_eq!(single.owner_id(), &alice);
303        assert!(single.may_add_members(&alice));
304        assert!(!single.may_add_members(&bob));
305        assert_eq!(single.admin_count(), 0);
306
307        let with_admins = ContractGroupOwner::OwnerAndAdmins {
308            owner: alice,
309            admins: BTreeSet::from([bob]),
310        };
311        assert_eq!(with_admins.owner_id(), &alice);
312        assert!(with_admins.may_add_members(&alice));
313        assert!(with_admins.may_add_members(&bob));
314        assert!(!with_admins.may_add_members(&carol));
315        assert_eq!(with_admins.admin_count(), 1);
316    }
317
318    #[test]
319    fn should_store_a_single_owner_when_the_registration_names_no_admins() {
320        let owner_id = Identifier::from([1u8; 32]);
321        let info: ContractGroupInfo = (
322            owner_id,
323            ContractGroupRegistration {
324                admins: BTreeSet::new(),
325                name: None,
326                description: None,
327            },
328        )
329            .into();
330        assert_eq!(info.owner(), &ContractGroupOwner::SingleOwner(owner_id));
331    }
332
333    #[test]
334    fn should_round_trip_the_stored_info_through_bincode() {
335        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
336
337        let info: ContractGroupInfo = (
338            Identifier::from([1u8; 32]),
339            ContractGroupRegistration {
340                admins: BTreeSet::from([Identifier::from([2u8; 32])]),
341                name: Some("cardgame".to_string()),
342                description: None,
343            },
344        )
345            .into();
346
347        let bytes = info.serialize_to_bytes().expect("serialize");
348        let decoded = ContractGroupInfo::deserialize_from_bytes_untrusted(&bytes)
349            .expect("deserialize untrusted");
350
351        assert_eq!(decoded, info);
352        assert_eq!(decoded.name(), Some("cardgame"));
353        assert_eq!(decoded.description(), None);
354        assert_eq!(decoded.owner().owner_id(), &Identifier::from([1u8; 32]));
355        assert_eq!(decoded.owner().admin_count(), 1);
356    }
357}