dpp/document/v0/
cbor_conversion.rs

1use crate::document::property_names;
2
3use crate::identity::TimestampMillis;
4use crate::prelude::{BlockHeight, CoreBlockHeight, Revision};
5
6use crate::ProtocolError;
7
8use crate::document::serialization_traits::{
9    DocumentCborMethodsV0, DocumentPlatformValueMethodsV0,
10};
11use crate::document::v0::DocumentV0;
12use crate::version::PlatformVersion;
13use ciborium::Value as CborValue;
14use integer_encoding::VarIntWriter;
15use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper;
16use platform_value::{Identifier, Value};
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19use std::convert::{TryFrom, TryInto};
20
21#[cfg(feature = "cbor")]
22#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
23pub struct DocumentForCbor {
24    /// The unique document ID.
25    #[serde(rename = "$id")]
26    pub id: [u8; 32],
27
28    /// The document's properties (data).
29    #[serde(flatten)]
30    pub properties: BTreeMap<String, CborValue>,
31
32    /// The ID of the document's owner.
33    #[serde(rename = "$ownerId")]
34    pub owner_id: [u8; 32],
35
36    /// The document revision.
37    #[serde(rename = "$revision")]
38    pub revision: Option<Revision>,
39
40    #[serde(rename = "$createdAt")]
41    pub created_at: Option<TimestampMillis>,
42    #[serde(rename = "$updatedAt")]
43    pub updated_at: Option<TimestampMillis>,
44    #[serde(rename = "$transferredAt")]
45    pub transferred_at: Option<TimestampMillis>,
46
47    #[serde(rename = "$createdAtBlockHeight")]
48    pub created_at_block_height: Option<BlockHeight>,
49    #[serde(rename = "$updatedAtBlockHeight")]
50    pub updated_at_block_height: Option<BlockHeight>,
51    #[serde(rename = "$transferredAtBlockHeight")]
52    pub transferred_at_block_height: Option<BlockHeight>,
53
54    #[serde(rename = "$createdAtCoreBlockHeight")]
55    pub created_at_core_block_height: Option<CoreBlockHeight>,
56    #[serde(rename = "$updatedAtCoreBlockHeight")]
57    pub updated_at_core_block_height: Option<CoreBlockHeight>,
58    #[serde(rename = "$transferredAtCoreBlockHeight")]
59    pub transferred_at_core_block_height: Option<CoreBlockHeight>,
60
61    #[serde(rename = "$creatorId")]
62    pub creator_id: Option<Identifier>,
63}
64
65#[cfg(feature = "cbor")]
66impl TryFrom<DocumentV0> for DocumentForCbor {
67    type Error = ProtocolError;
68
69    fn try_from(value: DocumentV0) -> Result<Self, Self::Error> {
70        let DocumentV0 {
71            id,
72            properties,
73            owner_id,
74            revision,
75            created_at,
76            updated_at,
77            transferred_at,
78            created_at_block_height,
79            updated_at_block_height,
80            transferred_at_block_height,
81            created_at_core_block_height,
82            updated_at_core_block_height,
83            transferred_at_core_block_height,
84            creator_id,
85        } = value;
86        Ok(DocumentForCbor {
87            id: id.to_buffer(),
88            properties: Value::convert_to_cbor_map(properties)
89                .map_err(ProtocolError::ValueError)?,
90            owner_id: owner_id.to_buffer(),
91            revision,
92            created_at,
93            updated_at,
94            transferred_at,
95            created_at_block_height,
96            updated_at_block_height,
97            transferred_at_block_height,
98            created_at_core_block_height,
99            updated_at_core_block_height,
100            transferred_at_core_block_height,
101            creator_id,
102        })
103    }
104}
105
106impl DocumentV0 {
107    /// Reads a CBOR-serialized document and creates a Document from it.
108    /// If Document and Owner IDs are provided, they are used, otherwise they are created.
109    fn from_map(
110        mut document_map: BTreeMap<String, Value>,
111        document_id: Option<[u8; 32]>,
112        owner_id: Option<[u8; 32]>,
113    ) -> Result<Self, ProtocolError> {
114        let owner_id = match owner_id {
115            None => document_map
116                .remove_hash256_bytes(property_names::OWNER_ID)
117                .map_err(ProtocolError::ValueError)?,
118            Some(owner_id) => owner_id,
119        };
120
121        let id = match document_id {
122            None => document_map
123                .remove_hash256_bytes(property_names::ID)
124                .map_err(ProtocolError::ValueError)?,
125            Some(document_id) => document_id,
126        };
127
128        let revision = document_map.remove_optional_integer(property_names::REVISION)?;
129
130        let created_at = document_map.remove_optional_integer(property_names::CREATED_AT)?;
131        let updated_at = document_map.remove_optional_integer(property_names::UPDATED_AT)?;
132        let transferred_at =
133            document_map.remove_optional_integer(property_names::TRANSFERRED_AT)?;
134        let created_at_block_height =
135            document_map.remove_optional_integer(property_names::CREATED_AT_BLOCK_HEIGHT)?;
136        let updated_at_block_height =
137            document_map.remove_optional_integer(property_names::UPDATED_AT_BLOCK_HEIGHT)?;
138        let transferred_at_block_height =
139            document_map.remove_optional_integer(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)?;
140        let created_at_core_block_height =
141            document_map.remove_optional_integer(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)?;
142        let updated_at_core_block_height =
143            document_map.remove_optional_integer(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)?;
144        let transferred_at_core_block_height = document_map
145            .remove_optional_integer(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)?;
146
147        let creator_id = document_map
148            .remove_optional_identifier(property_names::CREATOR_ID)
149            .map_err(ProtocolError::ValueError)?;
150
151        // dev-note: properties is everything other than the id and owner id
152        Ok(DocumentV0 {
153            properties: document_map,
154            owner_id: Identifier::new(owner_id),
155            id: Identifier::new(id),
156            revision,
157            created_at,
158            updated_at,
159            transferred_at,
160            created_at_block_height,
161            updated_at_block_height,
162            transferred_at_block_height,
163            created_at_core_block_height,
164            updated_at_core_block_height,
165            transferred_at_core_block_height,
166            creator_id,
167        })
168    }
169}
170
171impl DocumentCborMethodsV0 for DocumentV0 {
172    /// Reads a CBOR-serialized document and creates a Document from it.
173    /// If Document and Owner IDs are provided, they are used, otherwise they are created.
174    fn from_cbor(
175        document_cbor: &[u8],
176        document_id: Option<[u8; 32]>,
177        owner_id: Option<[u8; 32]>,
178        _platform_version: &PlatformVersion,
179    ) -> Result<Self, ProtocolError> {
180        // first we need to deserialize the document and contract indices
181        // we would need dedicated deserialization functions based on the document type
182        let document_cbor_map: BTreeMap<String, CborValue> =
183            ciborium::de::from_reader(document_cbor).map_err(|_| {
184                ProtocolError::InvalidCBOR(
185                    "unable to decode document for document call".to_string(),
186                )
187            })?;
188        let document_map: BTreeMap<String, Value> =
189            Value::convert_from_cbor_map(document_cbor_map).map_err(ProtocolError::ValueError)?;
190        Self::from_map(document_map, document_id, owner_id)
191    }
192
193    fn to_cbor_value(&self) -> Result<CborValue, ProtocolError> {
194        self.to_object()
195            .map(|v| v.try_into().map_err(ProtocolError::ValueError))?
196    }
197
198    /// Serializes the Document to CBOR.
199    fn to_cbor(&self) -> Result<Vec<u8>, ProtocolError> {
200        let mut buffer: Vec<u8> = Vec::new();
201        buffer.write_varint(0).map_err(|_| {
202            ProtocolError::EncodingError("error writing protocol version".to_string())
203        })?;
204        let cbor_document = DocumentForCbor::try_from(self.clone())?;
205        ciborium::ser::into_writer(&cbor_document, &mut buffer).map_err(|_| {
206            ProtocolError::EncodingError("unable to serialize into cbor".to_string())
207        })?;
208        Ok(buffer)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::data_contract::accessors::v0::DataContractV0Getters;
216    use crate::data_contract::document_type::random_document::CreateRandomDocument;
217    use crate::document::serialization_traits::DocumentCborMethodsV0;
218    use crate::document::DocumentV0Getters;
219    use crate::tests::json_document::json_document_to_contract;
220    use platform_version::version::PlatformVersion;
221
222    fn make_document_v0_with_timestamps() -> DocumentV0 {
223        let id = Identifier::new([1u8; 32]);
224        let owner_id = Identifier::new([2u8; 32]);
225        let mut properties = BTreeMap::new();
226        properties.insert("name".to_string(), Value::Text("Alice".to_string()));
227        properties.insert("age".to_string(), Value::U64(30));
228        DocumentV0 {
229            id,
230            owner_id,
231            properties,
232            revision: Some(1),
233            created_at: Some(1_700_000_000_000),
234            updated_at: Some(1_700_000_100_000),
235            transferred_at: None,
236            created_at_block_height: Some(100),
237            updated_at_block_height: Some(200),
238            transferred_at_block_height: None,
239            created_at_core_block_height: Some(50),
240            updated_at_core_block_height: Some(60),
241            transferred_at_core_block_height: None,
242            creator_id: None,
243        }
244    }
245
246    // ================================================================
247    //  Round-trip: to_cbor -> from_cbor preserves document data
248    // ================================================================
249
250    #[test]
251    fn cbor_round_trip_with_random_dashpay_profile() {
252        let platform_version = PlatformVersion::latest();
253        let contract = json_document_to_contract(
254            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
255            false,
256            platform_version,
257        )
258        .expect("expected to load dashpay contract");
259
260        let document_type = contract
261            .document_type_for_name("profile")
262            .expect("expected profile document type");
263
264        for seed in 0..10u64 {
265            let document = document_type
266                .random_document(Some(seed), platform_version)
267                .expect("expected random document");
268
269            // Use Document-level from_cbor which handles the version prefix
270            let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
271            let recovered =
272                crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version)
273                    .expect("from_cbor should succeed");
274
275            assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}");
276            assert_eq!(
277                document.owner_id(),
278                recovered.owner_id(),
279                "owner_id mismatch for seed {seed}"
280            );
281            assert_eq!(
282                document.revision(),
283                recovered.revision(),
284                "revision mismatch for seed {seed}"
285            );
286            assert_eq!(
287                document.properties(),
288                recovered.properties(),
289                "properties mismatch for seed {seed}"
290            );
291        }
292    }
293
294    #[test]
295    fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() {
296        let platform_version = PlatformVersion::latest();
297        let contract = json_document_to_contract(
298            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
299            false,
300            platform_version,
301        )
302        .expect("expected to load dashpay contract");
303
304        let document_type = contract
305            .document_type_for_name("profile")
306            .expect("expected profile document type");
307
308        let document = document_type
309            .random_document(Some(42), platform_version)
310            .expect("expected random document");
311
312        let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
313
314        let override_id = [0xAA; 32];
315        let override_owner = [0xBB; 32];
316
317        let recovered = crate::document::Document::from_cbor(
318            &cbor_bytes,
319            Some(override_id),
320            Some(override_owner),
321            platform_version,
322        )
323        .expect("from_cbor with explicit ids should succeed");
324
325        assert_eq!(
326            recovered.id(),
327            Identifier::new(override_id),
328            "explicit document_id should override the one in CBOR"
329        );
330        assert_eq!(
331            recovered.owner_id(),
332            Identifier::new(override_owner),
333            "explicit owner_id should override the one in CBOR"
334        );
335    }
336
337    // ================================================================
338    //  to_cbor_value produces a valid CborValue
339    // ================================================================
340
341    #[test]
342    fn to_cbor_value_returns_map_for_document_with_properties() {
343        let doc = make_document_v0_with_timestamps();
344        let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed");
345        // CborValue should be a Map at the top level
346        assert!(
347            cbor_val.is_map(),
348            "CBOR value of a document should be a Map, got {:?}",
349            cbor_val
350        );
351    }
352
353    // ================================================================
354    //  to_cbor output starts with varint-encoded version prefix (0)
355    // ================================================================
356
357    #[test]
358    fn to_cbor_starts_with_version_zero_varint() {
359        let doc = make_document_v0_with_timestamps();
360        let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed");
361        // The first byte should be the varint encoding of 0
362        assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty");
363        assert_eq!(
364            cbor_bytes[0], 0,
365            "first byte should be varint(0) for version"
366        );
367    }
368
369    // ================================================================
370    //  from_cbor rejects invalid CBOR data
371    // ================================================================
372
373    #[test]
374    fn from_cbor_rejects_invalid_cbor_bytes() {
375        let platform_version = PlatformVersion::latest();
376        let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
377        let result = DocumentV0::from_cbor(&garbage, None, None, platform_version);
378        assert!(
379            result.is_err(),
380            "from_cbor should fail on invalid CBOR bytes"
381        );
382    }
383
384    // ================================================================
385    //  DocumentForCbor TryFrom preserves all timestamp fields
386    // ================================================================
387
388    #[test]
389    fn document_for_cbor_preserves_all_fields() {
390        let doc = make_document_v0_with_timestamps();
391        let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed");
392        assert_eq!(cbor_doc.id, doc.id.to_buffer());
393        assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer());
394        assert_eq!(cbor_doc.revision, doc.revision);
395        assert_eq!(cbor_doc.created_at, doc.created_at);
396        assert_eq!(cbor_doc.updated_at, doc.updated_at);
397        assert_eq!(cbor_doc.transferred_at, doc.transferred_at);
398        assert_eq!(
399            cbor_doc.created_at_block_height,
400            doc.created_at_block_height
401        );
402        assert_eq!(
403            cbor_doc.updated_at_block_height,
404            doc.updated_at_block_height
405        );
406        assert_eq!(
407            cbor_doc.transferred_at_block_height,
408            doc.transferred_at_block_height
409        );
410        assert_eq!(
411            cbor_doc.created_at_core_block_height,
412            doc.created_at_core_block_height
413        );
414        assert_eq!(
415            cbor_doc.updated_at_core_block_height,
416            doc.updated_at_core_block_height
417        );
418        assert_eq!(
419            cbor_doc.transferred_at_core_block_height,
420            doc.transferred_at_core_block_height
421        );
422    }
423
424    // ================================================================
425    //  from_map populates fields correctly from a BTreeMap<String, Value>
426    // ================================================================
427
428    #[test]
429    fn from_map_extracts_system_fields_and_leaves_properties() {
430        let id_bytes = [3u8; 32];
431        let owner_bytes = [4u8; 32];
432
433        let mut map = BTreeMap::new();
434        map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes));
435        map.insert(
436            property_names::OWNER_ID.to_string(),
437            Value::Bytes32(owner_bytes),
438        );
439        map.insert(property_names::REVISION.to_string(), Value::U64(5));
440        map.insert(
441            property_names::CREATED_AT.to_string(),
442            Value::U64(1_000_000),
443        );
444        map.insert(
445            property_names::UPDATED_AT.to_string(),
446            Value::U64(2_000_000),
447        );
448        map.insert("customField".to_string(), Value::Text("hello".to_string()));
449
450        let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
451
452        assert_eq!(doc.id, Identifier::new(id_bytes));
453        assert_eq!(doc.owner_id, Identifier::new(owner_bytes));
454        assert_eq!(doc.revision, Some(5));
455        assert_eq!(doc.created_at, Some(1_000_000));
456        assert_eq!(doc.updated_at, Some(2_000_000));
457        // The custom field should remain in properties
458        assert_eq!(
459            doc.properties.get("customField"),
460            Some(&Value::Text("hello".to_string()))
461        );
462        // System fields should NOT be in properties
463        assert!(!doc.properties.contains_key(property_names::ID));
464        assert!(!doc.properties.contains_key(property_names::OWNER_ID));
465        assert!(!doc.properties.contains_key(property_names::REVISION));
466    }
467
468    #[test]
469    fn from_map_with_explicit_ids_overrides_map_ids() {
470        let map_id = [10u8; 32];
471        let map_owner = [11u8; 32];
472        let override_id = [20u8; 32];
473        let override_owner = [21u8; 32];
474
475        let mut map = BTreeMap::new();
476        map.insert(property_names::ID.to_string(), Value::Bytes32(map_id));
477        map.insert(
478            property_names::OWNER_ID.to_string(),
479            Value::Bytes32(map_owner),
480        );
481
482        let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner))
483            .expect("from_map should succeed");
484
485        assert_eq!(
486            doc.id,
487            Identifier::new(override_id),
488            "explicit document_id should take precedence"
489        );
490        assert_eq!(
491            doc.owner_id,
492            Identifier::new(override_owner),
493            "explicit owner_id should take precedence"
494        );
495    }
496
497    // ================================================================
498    //  Round-trip via from_map: construct map, parse, verify
499    // ================================================================
500
501    #[test]
502    fn from_map_with_all_timestamp_variants() {
503        let mut map = BTreeMap::new();
504        map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32]));
505        map.insert(
506            property_names::OWNER_ID.to_string(),
507            Value::Bytes32([6u8; 32]),
508        );
509        map.insert(
510            property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
511            Value::U64(100),
512        );
513        map.insert(
514            property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
515            Value::U64(200),
516        );
517        map.insert(
518            property_names::TRANSFERRED_AT.to_string(),
519            Value::U64(3_000_000),
520        );
521        map.insert(
522            property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
523            Value::U64(300),
524        );
525        map.insert(
526            property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
527            Value::U32(50),
528        );
529        map.insert(
530            property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
531            Value::U32(60),
532        );
533        map.insert(
534            property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
535            Value::U32(70),
536        );
537
538        let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
539
540        assert_eq!(doc.created_at_block_height, Some(100));
541        assert_eq!(doc.updated_at_block_height, Some(200));
542        assert_eq!(doc.transferred_at, Some(3_000_000));
543        assert_eq!(doc.transferred_at_block_height, Some(300));
544        assert_eq!(doc.created_at_core_block_height, Some(50));
545        assert_eq!(doc.updated_at_core_block_height, Some(60));
546        assert_eq!(doc.transferred_at_core_block_height, Some(70));
547    }
548}