Skip to main content

dpp/document/
generate_document_id.rs

1use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
2use crate::data_contract::document_type::DocumentTypeRef;
3use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
4use crate::prelude::IdentityNonce;
5use crate::ProtocolError;
6use crate::{prelude::Identifier, util::hash::hash_double, util::hash::hash_double_to_vec};
7use platform_version::version::PlatformVersion;
8
9/// Domain tag of the v1 document id preimage. A v0 preimage starts with a
10/// data contract id, which is itself a hash, so no v0 preimage can start with
11/// these bytes and the two derivations can never produce the same id from
12/// different inputs.
13const DOCUMENT_ID_V1_DOMAIN_TAG: &[u8] = b"dash:document-id:v1";
14
15impl Document {
16    /// Derives the id of a document that is about to be created.
17    ///
18    /// From `generate_document_id` version 1 the identity contract nonce of
19    /// the create transition is part of the id. A nonce is consumed at most
20    /// once per identity and contract, so an id can be produced at most once:
21    /// a document that was deleted can not be created again under the same
22    /// id with different content, and whatever referenced the id keeps
23    /// pointing at that one document or at nothing.
24    ///
25    /// The id therefore only exists once the nonce of the create transition
26    /// is known, and it changes if the transition is rebuilt with another
27    /// nonce.
28    pub fn generate_document_id(
29        contract_id: &Identifier,
30        owner_id: &Identifier,
31        document_type_name: &str,
32        entropy: &[u8],
33        identity_contract_nonce: IdentityNonce,
34        platform_version: &PlatformVersion,
35    ) -> Result<Identifier, ProtocolError> {
36        match platform_version
37            .dpp
38            .document_versions
39            .document_method_versions
40            .generate_document_id
41        {
42            0 => Ok(Self::generate_document_id_v0(
43                contract_id,
44                owner_id,
45                document_type_name,
46                entropy,
47            )),
48            1 => Ok(Self::generate_document_id_v1(
49                contract_id,
50                owner_id,
51                document_type_name,
52                entropy,
53                identity_contract_nonce,
54            )),
55            version => Err(ProtocolError::UnknownVersionMismatch {
56                method: "Document::generate_document_id".to_string(),
57                known_versions: vec![0, 1],
58                received: version,
59            }),
60        }
61    }
62
63    /// Gives a document that is about to be created the id its create
64    /// transition will carry, from the entropy and the identity contract nonce
65    /// that transition is going to use.
66    ///
67    /// A client that needs the id before it sends the document (to reference
68    /// it from another document, or to act on it afterwards) assigns the nonce
69    /// first and calls this. The create transition must then be built with the
70    /// same entropy and nonce.
71    pub fn set_id_for_creation(
72        &mut self,
73        document_type: DocumentTypeRef,
74        entropy: &[u8; 32],
75        identity_contract_nonce: IdentityNonce,
76        platform_version: &PlatformVersion,
77    ) -> Result<(), ProtocolError> {
78        let id = Self::generate_document_id(
79            &document_type.data_contract_id(),
80            &self.owner_id(),
81            document_type.name(),
82            entropy,
83            identity_contract_nonce,
84            platform_version,
85        )?;
86        self.set_id(id);
87        Ok(())
88    }
89
90    /// The number of SHA-256 blocks deriving the id takes, which is what
91    /// validating the id of a create is billed for.
92    ///
93    /// The entropy only id has always been billed as 2 blocks and stays so.
94    /// The nonce derived id is billed by what the double SHA-256 really
95    /// hashes: the padded preimage (longer than before by the domain tag and
96    /// the nonce) plus the one block of the second pass over the 32 byte
97    /// digest. That is 4 blocks for a document type name of up to 60 bytes
98    /// and 5 beyond that.
99    pub fn generate_document_id_sha256_blocks(
100        document_type_name: &str,
101        platform_version: &PlatformVersion,
102    ) -> Result<u16, ProtocolError> {
103        match platform_version
104            .dpp
105            .document_versions
106            .document_method_versions
107            .generate_document_id
108        {
109            0 => Ok(2),
110            1 => {
111                // tag + contract id + owner id + name + entropy + nonce, then
112                // the 0x80 byte and the 8 byte length SHA-256 pads with
113                let padded_len = DOCUMENT_ID_V1_DOMAIN_TAG.len()
114                    + 32
115                    + 32
116                    + document_type_name.len()
117                    + 32
118                    + 8
119                    + 9;
120                let first_pass_blocks = padded_len.div_ceil(64) as u16;
121                // the second pass hashes the 32 byte digest of the first
122                let second_pass_blocks = 1;
123                Ok(first_pass_blocks + second_pass_blocks)
124            }
125            version => Err(ProtocolError::UnknownVersionMismatch {
126                method: "Document::generate_document_id_sha256_blocks".to_string(),
127                known_versions: vec![0, 1],
128                received: version,
129            }),
130        }
131    }
132
133    /// Whether the id of a new document depends on the identity contract
134    /// nonce of its create transition. When it does, the id a document
135    /// carries before its create transition is built is only a placeholder.
136    pub fn document_id_depends_on_nonce(
137        platform_version: &PlatformVersion,
138    ) -> Result<bool, ProtocolError> {
139        match platform_version
140            .dpp
141            .document_versions
142            .document_method_versions
143            .generate_document_id
144        {
145            0 => Ok(false),
146            1 => Ok(true),
147            version => Err(ProtocolError::UnknownVersionMismatch {
148                method: "Document::document_id_depends_on_nonce".to_string(),
149                known_versions: vec![0, 1],
150                received: version,
151            }),
152        }
153    }
154
155    /// Generates the document ID
156    pub fn generate_document_id_v0(
157        contract_id: &Identifier,
158        owner_id: &Identifier,
159        document_type_name: &str,
160        entropy: &[u8],
161    ) -> Identifier {
162        let mut buf: Vec<u8> = vec![];
163
164        buf.extend_from_slice(&contract_id.to_buffer());
165        buf.extend_from_slice(&owner_id.to_buffer());
166        buf.extend_from_slice(document_type_name.as_bytes());
167        buf.extend_from_slice(entropy);
168
169        Identifier::from_bytes(&hash_double_to_vec(&buf)).unwrap()
170    }
171
172    /// Generates the document ID from the entropy and the identity contract
173    /// nonce of the create transition
174    pub fn generate_document_id_v1(
175        contract_id: &Identifier,
176        owner_id: &Identifier,
177        document_type_name: &str,
178        entropy: &[u8],
179        identity_contract_nonce: IdentityNonce,
180    ) -> Identifier {
181        let mut buf: Vec<u8> = Vec::with_capacity(
182            DOCUMENT_ID_V1_DOMAIN_TAG.len() + 64 + document_type_name.len() + entropy.len() + 8,
183        );
184
185        buf.extend_from_slice(DOCUMENT_ID_V1_DOMAIN_TAG);
186        buf.extend_from_slice(contract_id.as_slice());
187        buf.extend_from_slice(owner_id.as_slice());
188        buf.extend_from_slice(document_type_name.as_bytes());
189        buf.extend_from_slice(entropy);
190        buf.extend_from_slice(&identity_contract_nonce.to_be_bytes());
191
192        Identifier::from(hash_double(&buf))
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    const ENTROPY: [u8; 32] = [7u8; 32];
201
202    fn ids() -> (Identifier, Identifier) {
203        (Identifier::from([1u8; 32]), Identifier::from([2u8; 32]))
204    }
205
206    #[test]
207    fn should_derive_the_entropy_only_id_before_protocol_version_14() {
208        let (contract_id, owner_id) = ids();
209        let platform_version = PlatformVersion::get(13).expect("expected version 13");
210
211        let first = Document::generate_document_id(
212            &contract_id,
213            &owner_id,
214            "note",
215            &ENTROPY,
216            1,
217            platform_version,
218        )
219        .expect("expected an id");
220        let second = Document::generate_document_id(
221            &contract_id,
222            &owner_id,
223            "note",
224            &ENTROPY,
225            2,
226            platform_version,
227        )
228        .expect("expected an id");
229
230        assert_eq!(
231            first,
232            Document::generate_document_id_v0(&contract_id, &owner_id, "note", &ENTROPY)
233        );
234        assert_eq!(first, second);
235        assert!(
236            !Document::document_id_depends_on_nonce(platform_version).expect("expected a version")
237        );
238    }
239
240    #[test]
241    fn should_derive_a_different_id_for_every_nonce() {
242        let (contract_id, owner_id) = ids();
243        let platform_version = PlatformVersion::latest();
244
245        let first = Document::generate_document_id(
246            &contract_id,
247            &owner_id,
248            "note",
249            &ENTROPY,
250            1,
251            platform_version,
252        )
253        .expect("expected an id");
254        let second = Document::generate_document_id(
255            &contract_id,
256            &owner_id,
257            "note",
258            &ENTROPY,
259            2,
260            platform_version,
261        )
262        .expect("expected an id");
263
264        assert_ne!(first, second);
265        assert_ne!(
266            first,
267            Document::generate_document_id_v0(&contract_id, &owner_id, "note", &ENTROPY)
268        );
269        assert!(
270            Document::document_id_depends_on_nonce(platform_version).expect("expected a version")
271        );
272    }
273
274    #[test]
275    fn should_bill_the_nonce_derived_id_by_the_length_of_its_preimage() {
276        let latest = PlatformVersion::latest();
277        let version_13 = PlatformVersion::get(13).expect("expected version 13");
278        let blocks = |name: &str, version| {
279            Document::generate_document_id_sha256_blocks(name, version).expect("expected blocks")
280        };
281
282        // the entropy only id keeps the 2 blocks it was always billed
283        assert_eq!(blocks("note", version_13), 2);
284        assert_eq!(blocks(&"n".repeat(64), version_13), 2);
285
286        assert_eq!(blocks("note", latest), 4);
287        assert_eq!(blocks(&"n".repeat(60), latest), 4);
288        assert_eq!(blocks(&"n".repeat(61), latest), 5);
289    }
290
291    #[test]
292    fn should_keep_the_entropy_in_the_nonce_derived_id() {
293        let (contract_id, owner_id) = ids();
294
295        assert_ne!(
296            Document::generate_document_id_v1(&contract_id, &owner_id, "note", &ENTROPY, 1),
297            Document::generate_document_id_v1(&contract_id, &owner_id, "note", &[8u8; 32], 1),
298        );
299    }
300
301    #[test]
302    fn should_pin_the_nonce_derived_id() {
303        let (contract_id, owner_id) = ids();
304
305        // Every client derives this id on its own, so the preimage layout is
306        // part of the protocol: a change here is a consensus change.
307        assert_eq!(
308            Document::generate_document_id_v1(&contract_id, &owner_id, "note", &ENTROPY, 1)
309                .to_string(platform_value::string_encoding::Encoding::Hex),
310            PINNED_V1_ID
311        );
312    }
313
314    const PINNED_V1_ID: &str = "e574ae73396611a517691d1f89275b6e99642cb9c176ce8cf879b1665c50f15f";
315}