Skip to main content

p2panda_core/operation/
header.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use cbor_core::Value;
4
5use crate::hash::Hash;
6#[cfg(any(test, feature = "test_utils"))]
7use crate::identity::SigningKey;
8use crate::identity::{Signature, VerifyingKey};
9use crate::logs::SeqNum;
10use crate::operation::{AnyHeader, Builder};
11use crate::traits::{Chain, Digest, Extensions, Offchain, Provenance};
12use crate::{Body, HeaderError};
13
14/// Operation format version.
15pub type Version = u16;
16
17/// Number of bytes of the body of this operation.
18pub type PayloadSize = u32;
19
20/// Header of a p2panda operation with known extensions type.
21///
22/// The header holds all metadata required to cryptographically secure and authenticate a message
23/// [`Body`] and it's custom extensions.
24///
25/// See [`AnyHeader`] for dealing with headers when you don't care about the concrete extensions
26/// type (`E`).
27///
28/// ## Example
29///
30/// ```
31/// use p2panda_core::{Header, SigningKey};
32///
33/// let signing_key = SigningKey::generate();
34///
35/// let header = Header::builder()
36///     .body(b"Hello, Icebear!")
37///      // Sign the header with the author's private key.
38///     .build(&signing_key, ());
39/// ```
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct Header<E = ()> {
42    /// Operation format version, allowing backwards compatibility when specification changes.
43    pub version: Version,
44
45    /// Author of this operation.
46    pub verifying_key: VerifyingKey,
47
48    /// Signature by author over all fields in header, providing authenticity.
49    pub signature: Signature,
50
51    /// Number of bytes of the body of this operation, must be zero if no body is given.
52    pub payload_size: PayloadSize,
53
54    /// Hash of the body of this operation, must be included if payload_size is non-zero and
55    /// omitted otherwise.
56    ///
57    /// Keeping the hash here allows us to delete the payload (off-chain data) while retaining the
58    /// ability to check the signature of the header.
59    pub payload_hash: Option<Hash>,
60
61    /// Number of operations this author has published to this log, begins with 0 and is always
62    /// incremented by 1 with each new operation by the same author.
63    pub seq_num: SeqNum,
64
65    /// Hash of the previous operation of the same author and log. Can be omitted if first
66    /// operation in log.
67    pub backlink: Option<Hash>,
68
69    /// Custom additional data.
70    //
71    // NOTE: If `E` is a Zero-Sized Type (ZST) we use unsafe code to skip the redundant field when
72    // encoding or decoding the header. See `zero_sized_extensions` for safety details.
73    //
74    // This allows us to keep the usage of Header ergonomic while assuring operations are encoded
75    // most efficiently and correctly according to p2panda's specification.
76    //
77    // An alternative would be to make this field an `Option` or introduce `E: Default` bounds to
78    // allow initialisation in safe code which both are annoying to deal with.
79    pub extensions: E,
80
81    /// Original extensions representation in CBOR AST.
82    ///
83    /// This allows us to correctly re-encode this header to bytes if necessary. If we would encode
84    /// the extensions from the Rust type `E` we might not be able to re-construct the original
85    /// bytes. A different system might have interpreted the extensions differently.
86    pub(crate) extensions_cbor: Option<cbor_core::Value<'static>>,
87
88    /// Size of header in encoded CBOR bytes.
89    pub(crate) size: u32,
90
91    /// BLAKE3 hash digest of header.
92    pub(crate) digest: Hash,
93}
94
95impl<E> Header<E>
96where
97    E: Extensions,
98{
99    /// Returns builder to create & sign new header.
100    pub fn builder() -> Builder<E> {
101        Builder::new()
102    }
103
104    /// Encodes header to byte-representation (CBOR).
105    pub fn encode(&self) -> Vec<u8> {
106        encode_header(
107            self.version,
108            self.verifying_key,
109            Some(&self.signature),
110            self.payload_size,
111            self.payload_hash,
112            self.seq_num,
113            self.backlink,
114            self.extensions_cbor.as_ref(),
115        )
116    }
117
118    /// Attempts decoding header from bytes.
119    ///
120    /// This might fail if integrity checks failed or header formatting is invalid.
121    pub fn decode(bytes: &[u8]) -> Result<Self, HeaderError> {
122        // Decode header.
123        let any_header = AnyHeader::decode(bytes)?;
124
125        // Decode extensions.
126        Self::try_from(any_header)
127    }
128
129    /// BLAKE3 hash digest of the header bytes.
130    ///
131    /// This hash is used as the unique identifier of an operation, aka the Operation Id.
132    pub fn hash(&self) -> Hash {
133        // Re-calculate hash and size in test environments.
134        if cfg!(any(test, feature = "test_utils")) {
135            return Hash::digest(self.encode());
136        }
137
138        self.digest
139    }
140
141    /// Size of header when encoded as CBOR bytes.
142    pub fn size(&self) -> u32 {
143        // Re-calculate hash and size in test environments.
144        if cfg!(any(test, feature = "test_utils")) {
145            return self.encode().len() as u32;
146        }
147
148        self.size
149    }
150}
151
152impl<E> Header<E> {
153    pub(crate) const fn has_zero_sized_extensions() -> bool {
154        std::mem::size_of::<E>() == 0
155    }
156
157    pub(crate) fn zero_sized_extensions() -> E {
158        assert!(Self::has_zero_sized_extensions());
159
160        // SAFETY: The assertion guarantees E is a zero-sized type.
161        //
162        // For ZSTs, there are no bytes to initialize. std::mem::zeroed() on a ZST is a compile-time
163        // no-op with no actual memory operations.
164        unsafe { std::mem::zeroed() }
165    }
166}
167
168impl<E> Digest<Hash> for Header<E>
169where
170    E: Extensions,
171{
172    fn hash(&self) -> Hash {
173        self.hash()
174    }
175}
176
177impl<E> Provenance<VerifyingKey> for Header<E>
178where
179    E: Extensions,
180{
181    fn author(&self) -> VerifyingKey {
182        self.verifying_key
183    }
184
185    fn verify(&self) -> bool {
186        // Check signature in test environments as low-level access might have allowed users to
187        // tamper with the integrity.
188        if cfg!(any(test, feature = "test_utils")) {
189            return self.verify();
190        }
191
192        // Header was always created by us and has a valid signature.
193        true
194    }
195}
196
197impl<E> Chain<Hash> for Header<E>
198where
199    E: Extensions,
200{
201    fn backlink(&self) -> Option<Hash> {
202        self.backlink
203    }
204
205    fn seq_num(&self) -> SeqNum {
206        self.seq_num
207    }
208}
209
210impl<E> Offchain<Hash> for Header<E>
211where
212    E: Extensions,
213{
214    fn payload(&self) -> Option<&Body> {
215        None // We don't have the body here.
216    }
217
218    fn payload_hash(&self) -> Option<Hash> {
219        self.payload_hash
220    }
221
222    fn payload_size(&self) -> PayloadSize {
223        self.payload_size
224    }
225}
226
227#[allow(clippy::too_many_arguments)]
228pub(crate) fn encode_header(
229    version: Version,
230    verifying_key: VerifyingKey,
231    signature: Option<&Signature>,
232    payload_size: PayloadSize,
233    payload_hash: Option<Hash>,
234    seq_num: SeqNum,
235    backlink: Option<Hash>,
236    extensions: Option<&Value<'static>>,
237) -> Vec<u8> {
238    let mut cbor = Value::array([Value::from(version), Value::from(verifying_key.as_bytes())]);
239
240    // Signature can be omitted to encode bytes for signing.
241    if let Some(signature) = &signature {
242        cbor.append(signature.to_bytes());
243    }
244
245    cbor.append(payload_size);
246
247    if let Some(payload_hash) = &payload_hash {
248        cbor.append(payload_hash.as_bytes());
249    }
250
251    cbor.append(seq_num);
252
253    if let Some(backlink) = &backlink {
254        cbor.append(backlink.as_bytes());
255    }
256
257    // We're serializing from the AST using cbor_core. If decoding an extension from another
258    // code-base (which was generated using another CBOR encoder with different rules) and encoding
259    // it here again, we might end up with a different byte sequence and thus hash digest.
260    //
261    // This can for example happen if the given extension uses non-canonical CBOR encoding,
262    // ambigious map ordering etc.
263    //
264    // To mitigate this from happening we're enforcing a strict, canonical CBOR encoding when
265    // decoding the extensions bytes.
266    if let Some(extensions) = extensions {
267        cbor.append(extensions.to_owned());
268    }
269
270    cbor.encode()
271}
272
273impl<E> TryFrom<AnyHeader> for Header<E>
274where
275    E: Extensions,
276{
277    type Error = HeaderError;
278
279    fn try_from(value: AnyHeader) -> Result<Self, Self::Error> {
280        let extensions = match value.extensions {
281            Some(ref cbor) => {
282                // For ZST extension types we don't expect the extensions field in the header to be
283                // set. Since we now know E we can assure that this is the case.
284                if Header::<E>::has_zero_sized_extensions() {
285                    return Err(HeaderError::UnexpectedExtensions);
286                }
287
288                // At this point we've already decoded the byte string into CBOR. Now we only need
289                // serde to iterate over these values to check if they match the given Rust type.
290                cbor.deserialized()
291                    .map_err(HeaderError::DecodingExtensions)?
292            }
293            None => {
294                if !Header::<E>::has_zero_sized_extensions() {
295                    return Err(HeaderError::MissingExtensions);
296                } else {
297                    Header::<E>::zero_sized_extensions()
298                }
299            }
300        };
301
302        Ok(Header {
303            version: value.version,
304            verifying_key: value.verifying_key,
305            signature: value.signature,
306            payload_size: value.payload_size,
307            payload_hash: value.payload_hash,
308            seq_num: value.seq_num,
309            backlink: value.backlink,
310            extensions,
311            extensions_cbor: value.extensions,
312            size: value.size,
313            digest: value.digest,
314        })
315    }
316}
317
318#[cfg(any(test, feature = "test_utils"))]
319impl<E> Default for Header<E>
320where
321    E: Default,
322{
323    /// This is for hacky low-level access to this type, don't use this in production.
324    ///
325    /// Size and digest get re-computed whenever called in test environments. Note that we can't
326    /// re-encode `extensions_cbor` if `E` was changed in a test. Ideally you don't want to test
327    /// extensions-related code here anyway.
328    fn default() -> Self {
329        use crate::hash::HASH_LEN;
330        use crate::identity::SIGNATURE_LEN;
331
332        Self {
333            version: 1,
334            verifying_key: VerifyingKey::default(),
335            signature: Signature::from([0; SIGNATURE_LEN]),
336            payload_size: 0,
337            payload_hash: None,
338            seq_num: 0,
339            backlink: None,
340            extensions: E::default(),
341            extensions_cbor: None,
342            size: 0,
343            digest: Hash::from([0; HASH_LEN]),
344        }
345    }
346}
347
348#[cfg(any(test, feature = "test_utils"))]
349impl<E> Header<E>
350where
351    E: Extensions,
352{
353    pub fn to_hex(&self) -> String {
354        hex::encode(self.encode())
355    }
356
357    fn encode_signing_bytes(&self) -> Vec<u8> {
358        encode_header(
359            self.version,
360            self.verifying_key,
361            None,
362            self.payload_size,
363            self.payload_hash,
364            self.seq_num,
365            self.backlink,
366            self.extensions_cbor.as_ref(),
367        )
368    }
369
370    pub fn sign(&mut self, signer: &SigningKey) {
371        let signing_bytes = self.encode_signing_bytes();
372        self.signature = signer.sign(&signing_bytes);
373        self.update_size_and_digest();
374    }
375
376    pub fn verify(&self) -> bool {
377        let signing_bytes = self.encode_signing_bytes();
378        self.verifying_key.verify(&signing_bytes, &self.signature)
379    }
380
381    fn update_size_and_digest(&mut self) {
382        self.size = self.size();
383        self.digest = self.hash();
384    }
385}
386
387#[cfg(feature = "arbitrary")]
388impl<'a, E> arbitrary::Arbitrary<'a> for Header<E>
389where
390    E: Default + Extensions,
391{
392    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
393        use crate::hash::HASH_LEN;
394        use crate::identity::SIGNATURE_LEN;
395
396        let header = Header {
397            version: 1,
398            verifying_key: u.arbitrary()?,
399            signature: Signature::from_bytes(&[0; SIGNATURE_LEN]),
400            payload_size: u.arbitrary()?,
401            payload_hash: u.arbitrary()?,
402            seq_num: u.arbitrary()?,
403            backlink: u.arbitrary()?,
404            extensions: E::default(),
405            extensions_cbor: None,
406            size: 0,
407            digest: Hash::from_bytes([0; HASH_LEN]),
408        };
409
410        Ok(header)
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::Header;
417
418    #[test]
419    fn zst_size_matches_mem_checks() {
420        struct ZstExtensions;
421        assert_eq!(std::mem::size_of::<ZstExtensions>(), 0);
422        assert!(Header::<ZstExtensions>::has_zero_sized_extensions());
423
424        #[allow(unused)]
425        struct NonZstExtensions(u32);
426        assert_ne!(std::mem::size_of::<NonZstExtensions>(), 0);
427        assert!(!Header::<NonZstExtensions>::has_zero_sized_extensions());
428    }
429}