|
| 1 | +//! Document Payload Chain. |
| 2 | +//! |
| 3 | +//! ref: <https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/metadata/#chain-link> |
| 4 | +
|
| 5 | +use std::{fmt::Display, hash::Hash}; |
| 6 | + |
| 7 | +use cbork_utils::{array::Array, decode_context::DecodeCtx}; |
| 8 | + |
| 9 | +use crate::DocumentRef; |
| 10 | + |
| 11 | +/// Reference to the previous Signed Document in a sequence. |
| 12 | +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] |
| 13 | +pub struct Chain { |
| 14 | + /// The consecutive sequence number of the current document |
| 15 | + /// in the chain. |
| 16 | + /// The very first document in a sequence is numbered `0` and it |
| 17 | + /// *MUST ONLY* increment by one for each successive document in |
| 18 | + /// the sequence. |
| 19 | + /// |
| 20 | + /// The FINAL sequence number is encoded with the current height |
| 21 | + /// sequence value, negated. |
| 22 | + /// |
| 23 | + /// For example the following values for height define a chain |
| 24 | + /// that has 5 documents in the sequence 0-4, the final height |
| 25 | + /// is negated to indicate the end of the chain: |
| 26 | + /// `0, 1, 2, 3, -4` |
| 27 | + /// |
| 28 | + /// No subsequent document can be chained to a sequence that has |
| 29 | + /// a final chain height. |
| 30 | + height: i32, |
| 31 | + /// Reference to a single Signed Document. |
| 32 | + /// |
| 33 | + /// Can be *ONLY* omitted in the very first document in a sequence. |
| 34 | + document_ref: Option<DocumentRef>, |
| 35 | +} |
| 36 | + |
| 37 | +impl Display for Chain { |
| 38 | + fn fmt( |
| 39 | + &self, |
| 40 | + f: &mut std::fmt::Formatter<'_>, |
| 41 | + ) -> std::fmt::Result { |
| 42 | + if let Some(document_ref) = &self.document_ref { |
| 43 | + write!(f, "height: {}, document_ref: {}", self.height, document_ref) |
| 44 | + } else { |
| 45 | + write!(f, "height: {}", self.height) |
| 46 | + } |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +impl minicbor::Encode<()> for Chain { |
| 51 | + fn encode<W: minicbor::encode::Write>( |
| 52 | + &self, |
| 53 | + e: &mut minicbor::Encoder<W>, |
| 54 | + _ctx: &mut (), |
| 55 | + ) -> Result<(), minicbor::encode::Error<W::Error>> { |
| 56 | + e.array(if self.document_ref.is_some() { 2 } else { 1 })?; |
| 57 | + self.height.encode(e, &mut ())?; |
| 58 | + if let Some(doc_ref) = &self.document_ref { |
| 59 | + doc_ref.encode(e, &mut ())?; |
| 60 | + } |
| 61 | + Ok(()) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +impl minicbor::Decode<'_, ()> for Chain { |
| 66 | + fn decode( |
| 67 | + d: &mut minicbor::Decoder<'_>, |
| 68 | + _ctx: &mut (), |
| 69 | + ) -> Result<Self, minicbor::decode::Error> { |
| 70 | + const CONTEXT: &str = "Chain decoding"; |
| 71 | + |
| 72 | + let arr = Array::decode(d, &mut DecodeCtx::Deterministic)?; |
| 73 | + |
| 74 | + let Some(height_bytes) = arr.first() else { |
| 75 | + return Err(minicbor::decode::Error::message(format!( |
| 76 | + "{CONTEXT}: expected [height, ? document_ref], found empty array" |
| 77 | + ))); |
| 78 | + }; |
| 79 | + |
| 80 | + let height = minicbor::Decoder::new(height_bytes).int()?; |
| 81 | + let height = height.try_into().map_err(minicbor::decode::Error::custom)?; |
| 82 | + |
| 83 | + let document_ref = arr |
| 84 | + .get(1) |
| 85 | + .map(|bytes| { |
| 86 | + let mut d = minicbor::Decoder::new(bytes); |
| 87 | + DocumentRef::decode(&mut d, &mut ()) |
| 88 | + }) |
| 89 | + .transpose()?; |
| 90 | + |
| 91 | + Ok(Self { |
| 92 | + height, |
| 93 | + document_ref, |
| 94 | + }) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +#[cfg(test)] |
| 99 | +mod tests { |
| 100 | + use catalyst_types::uuid::UuidV7; |
| 101 | + use minicbor::{Decode, Decoder, Encode, Encoder}; |
| 102 | + |
| 103 | + use super::*; |
| 104 | + use crate::DocLocator; |
| 105 | + |
| 106 | + #[test] |
| 107 | + fn test_chain_encode_decode_without_doc_ref() { |
| 108 | + let chain = Chain { |
| 109 | + height: 0, |
| 110 | + document_ref: None, |
| 111 | + }; |
| 112 | + |
| 113 | + let mut buf = Vec::new(); |
| 114 | + let mut enc = Encoder::new(&mut buf); |
| 115 | + chain.encode(&mut enc, &mut ()).unwrap(); |
| 116 | + |
| 117 | + let mut dec = Decoder::new(&buf); |
| 118 | + let decoded = Chain::decode(&mut dec, &mut ()).unwrap(); |
| 119 | + |
| 120 | + assert_eq!(decoded, chain); |
| 121 | + } |
| 122 | + |
| 123 | + #[test] |
| 124 | + fn test_chain_encode_decode_with_doc_ref() { |
| 125 | + let id = UuidV7::new(); |
| 126 | + let ver = UuidV7::new(); |
| 127 | + |
| 128 | + let chain = Chain { |
| 129 | + height: 3, |
| 130 | + document_ref: Some(DocumentRef::new(id, ver, DocLocator::default())), |
| 131 | + }; |
| 132 | + |
| 133 | + let mut buf = Vec::new(); |
| 134 | + let mut enc = Encoder::new(&mut buf); |
| 135 | + chain.encode(&mut enc, &mut ()).unwrap(); |
| 136 | + |
| 137 | + let mut dec = Decoder::new(&buf); |
| 138 | + let decoded = Chain::decode(&mut dec, &mut ()).unwrap(); |
| 139 | + |
| 140 | + assert_eq!(decoded, chain); |
| 141 | + } |
| 142 | +} |
0 commit comments