p2panda-core 0.7.1

Extensible data-types for secure, distributed and efficient exchange of data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
// SPDX-License-Identifier: MIT OR Apache-2.0

use cbor_core::Value;

use crate::hash::Hash;
#[cfg(any(test, feature = "test_utils"))]
use crate::identity::SigningKey;
use crate::identity::{Signature, VerifyingKey};
use crate::logs::SeqNum;
use crate::operation::{AnyHeader, Builder};
use crate::traits::{Chain, Digest, Extensions, Offchain, Provenance};
use crate::{Body, HeaderError};

/// Operation format version.
pub type Version = u16;

/// Number of bytes of the body of this operation.
pub type PayloadSize = u32;

/// Header of a p2panda operation with known extensions type.
///
/// The header holds all metadata required to cryptographically secure and authenticate a message
/// [`Body`] and it's custom extensions.
///
/// See [`AnyHeader`] for dealing with headers when you don't care about the concrete extensions
/// type (`E`).
///
/// ## Example
///
/// ```
/// use p2panda_core::{Header, SigningKey};
///
/// let signing_key = SigningKey::generate();
///
/// let header = Header::builder()
///     .body(b"Hello, Icebear!")
///      // Sign the header with the author's private key.
///     .build(&signing_key, ());
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Header<E = ()> {
    /// Operation format version, allowing backwards compatibility when specification changes.
    pub version: Version,

    /// Author of this operation.
    pub verifying_key: VerifyingKey,

    /// Signature by author over all fields in header, providing authenticity.
    pub signature: Signature,

    /// Number of bytes of the body of this operation, must be zero if no body is given.
    pub payload_size: PayloadSize,

    /// Hash of the body of this operation, must be included if payload_size is non-zero and
    /// omitted otherwise.
    ///
    /// Keeping the hash here allows us to delete the payload (off-chain data) while retaining the
    /// ability to check the signature of the header.
    pub payload_hash: Option<Hash>,

    /// Number of operations this author has published to this log, begins with 0 and is always
    /// incremented by 1 with each new operation by the same author.
    pub seq_num: SeqNum,

    /// Hash of the previous operation of the same author and log. Can be omitted if first
    /// operation in log.
    pub backlink: Option<Hash>,

    /// Custom additional data.
    //
    // NOTE: If `E` is a Zero-Sized Type (ZST) we use unsafe code to skip the redundant field when
    // encoding or decoding the header. See `zero_sized_extensions` for safety details.
    //
    // This allows us to keep the usage of Header ergonomic while assuring operations are encoded
    // most efficiently and correctly according to p2panda's specification.
    //
    // An alternative would be to make this field an `Option` or introduce `E: Default` bounds to
    // allow initialisation in safe code which both are annoying to deal with.
    pub extensions: E,

    /// Original extensions representation in CBOR AST.
    ///
    /// This allows us to correctly re-encode this header to bytes if necessary. If we would encode
    /// the extensions from the Rust type `E` we might not be able to re-construct the original
    /// bytes. A different system might have interpreted the extensions differently.
    pub(crate) extensions_cbor: Option<cbor_core::Value<'static>>,

    /// Size of header in encoded CBOR bytes.
    pub(crate) size: u32,

    /// BLAKE3 hash digest of header.
    pub(crate) digest: Hash,
}

impl<E> Header<E>
where
    E: Extensions,
{
    /// Returns builder to create & sign new header.
    pub fn builder() -> Builder<E> {
        Builder::new()
    }

    /// Encodes header to byte-representation (CBOR).
    pub fn encode(&self) -> Vec<u8> {
        encode_header(
            self.version,
            self.verifying_key,
            Some(&self.signature),
            self.payload_size,
            self.payload_hash,
            self.seq_num,
            self.backlink,
            self.extensions_cbor.as_ref(),
        )
    }

    /// Attempts decoding header from bytes.
    ///
    /// This might fail if integrity checks failed or header formatting is invalid.
    pub fn decode(bytes: &[u8]) -> Result<Self, HeaderError> {
        // Decode header.
        let any_header = AnyHeader::decode(bytes)?;

        // Decode extensions.
        Self::try_from(any_header)
    }

    /// BLAKE3 hash digest of the header bytes.
    ///
    /// This hash is used as the unique identifier of an operation, aka the Operation Id.
    pub fn hash(&self) -> Hash {
        // Re-calculate hash and size in test environments.
        if cfg!(any(test, feature = "test_utils")) {
            return Hash::digest(self.encode());
        }

        self.digest
    }

    /// Size of header when encoded as CBOR bytes.
    pub fn size(&self) -> u32 {
        // Re-calculate hash and size in test environments.
        if cfg!(any(test, feature = "test_utils")) {
            return self.encode().len() as u32;
        }

        self.size
    }
}

impl<E> Header<E> {
    pub(crate) const fn has_zero_sized_extensions() -> bool {
        std::mem::size_of::<E>() == 0
    }

    pub(crate) fn zero_sized_extensions() -> E {
        assert!(Self::has_zero_sized_extensions());

        // SAFETY: The assertion guarantees E is a zero-sized type.
        //
        // For ZSTs, there are no bytes to initialize. std::mem::zeroed() on a ZST is a compile-time
        // no-op with no actual memory operations.
        unsafe { std::mem::zeroed() }
    }
}

impl<E> Digest<Hash> for Header<E>
where
    E: Extensions,
{
    fn hash(&self) -> Hash {
        self.hash()
    }
}

impl<E> Provenance<VerifyingKey> for Header<E>
where
    E: Extensions,
{
    fn author(&self) -> VerifyingKey {
        self.verifying_key
    }

    fn verify(&self) -> bool {
        // Check signature in test environments as low-level access might have allowed users to
        // tamper with the integrity.
        if cfg!(any(test, feature = "test_utils")) {
            return self.verify();
        }

        // Header was always created by us and has a valid signature.
        true
    }
}

impl<E> Chain<Hash> for Header<E>
where
    E: Extensions,
{
    fn backlink(&self) -> Option<Hash> {
        self.backlink
    }

    fn seq_num(&self) -> SeqNum {
        self.seq_num
    }
}

impl<E> Offchain<Hash> for Header<E>
where
    E: Extensions,
{
    fn payload(&self) -> Option<&Body> {
        None // We don't have the body here.
    }

    fn payload_hash(&self) -> Option<Hash> {
        self.payload_hash
    }

    fn payload_size(&self) -> PayloadSize {
        self.payload_size
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_header(
    version: Version,
    verifying_key: VerifyingKey,
    signature: Option<&Signature>,
    payload_size: PayloadSize,
    payload_hash: Option<Hash>,
    seq_num: SeqNum,
    backlink: Option<Hash>,
    extensions: Option<&Value<'static>>,
) -> Vec<u8> {
    let mut cbor = Value::array([Value::from(version), Value::from(verifying_key.as_bytes())]);

    // Signature can be omitted to encode bytes for signing.
    if let Some(signature) = &signature {
        cbor.append(signature.to_bytes());
    }

    cbor.append(payload_size);

    if let Some(payload_hash) = &payload_hash {
        cbor.append(payload_hash.as_bytes());
    }

    cbor.append(seq_num);

    if let Some(backlink) = &backlink {
        cbor.append(backlink.as_bytes());
    }

    // We're serializing from the AST using cbor_core. If decoding an extension from another
    // code-base (which was generated using another CBOR encoder with different rules) and encoding
    // it here again, we might end up with a different byte sequence and thus hash digest.
    //
    // This can for example happen if the given extension uses non-canonical CBOR encoding,
    // ambigious map ordering etc.
    //
    // To mitigate this from happening we're enforcing a strict, canonical CBOR encoding when
    // decoding the extensions bytes.
    if let Some(extensions) = extensions {
        cbor.append(extensions.to_owned());
    }

    cbor.encode()
}

impl<E> TryFrom<AnyHeader> for Header<E>
where
    E: Extensions,
{
    type Error = HeaderError;

    fn try_from(value: AnyHeader) -> Result<Self, Self::Error> {
        let extensions = match value.extensions {
            Some(ref cbor) => {
                // For ZST extension types we don't expect the extensions field in the header to be
                // set. Since we now know E we can assure that this is the case.
                if Header::<E>::has_zero_sized_extensions() {
                    return Err(HeaderError::UnexpectedExtensions);
                }

                // At this point we've already decoded the byte string into CBOR. Now we only need
                // serde to iterate over these values to check if they match the given Rust type.
                cbor.deserialized()
                    .map_err(HeaderError::DecodingExtensions)?
            }
            None => {
                if !Header::<E>::has_zero_sized_extensions() {
                    return Err(HeaderError::MissingExtensions);
                } else {
                    Header::<E>::zero_sized_extensions()
                }
            }
        };

        Ok(Header {
            version: value.version,
            verifying_key: value.verifying_key,
            signature: value.signature,
            payload_size: value.payload_size,
            payload_hash: value.payload_hash,
            seq_num: value.seq_num,
            backlink: value.backlink,
            extensions,
            extensions_cbor: value.extensions,
            size: value.size,
            digest: value.digest,
        })
    }
}

#[cfg(any(test, feature = "test_utils"))]
impl<E> Default for Header<E>
where
    E: Default,
{
    /// This is for hacky low-level access to this type, don't use this in production.
    ///
    /// Size and digest get re-computed whenever called in test environments. Note that we can't
    /// re-encode `extensions_cbor` if `E` was changed in a test. Ideally you don't want to test
    /// extensions-related code here anyway.
    fn default() -> Self {
        use crate::hash::HASH_LEN;
        use crate::identity::SIGNATURE_LEN;

        Self {
            version: 1,
            verifying_key: VerifyingKey::default(),
            signature: Signature::from([0; SIGNATURE_LEN]),
            payload_size: 0,
            payload_hash: None,
            seq_num: 0,
            backlink: None,
            extensions: E::default(),
            extensions_cbor: None,
            size: 0,
            digest: Hash::from([0; HASH_LEN]),
        }
    }
}

#[cfg(any(test, feature = "test_utils"))]
impl<E> Header<E>
where
    E: Extensions,
{
    pub fn to_hex(&self) -> String {
        hex::encode(self.encode())
    }

    fn encode_signing_bytes(&self) -> Vec<u8> {
        encode_header(
            self.version,
            self.verifying_key,
            None,
            self.payload_size,
            self.payload_hash,
            self.seq_num,
            self.backlink,
            self.extensions_cbor.as_ref(),
        )
    }

    pub fn sign(&mut self, signer: &SigningKey) {
        let signing_bytes = self.encode_signing_bytes();
        self.signature = signer.sign(&signing_bytes);
        self.update_size_and_digest();
    }

    pub fn verify(&self) -> bool {
        let signing_bytes = self.encode_signing_bytes();
        self.verifying_key.verify(&signing_bytes, &self.signature)
    }

    fn update_size_and_digest(&mut self) {
        self.size = self.size();
        self.digest = self.hash();
    }
}

#[cfg(feature = "arbitrary")]
impl<'a, E> arbitrary::Arbitrary<'a> for Header<E>
where
    E: Default + Extensions,
{
    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
        use crate::hash::HASH_LEN;
        use crate::identity::SIGNATURE_LEN;

        let header = Header {
            version: 1,
            verifying_key: u.arbitrary()?,
            signature: Signature::from_bytes(&[0; SIGNATURE_LEN]),
            payload_size: u.arbitrary()?,
            payload_hash: u.arbitrary()?,
            seq_num: u.arbitrary()?,
            backlink: u.arbitrary()?,
            extensions: E::default(),
            extensions_cbor: None,
            size: 0,
            digest: Hash::from_bytes([0; HASH_LEN]),
        };

        Ok(header)
    }
}

#[cfg(test)]
mod tests {
    use super::Header;

    #[test]
    fn zst_size_matches_mem_checks() {
        struct ZstExtensions;
        assert_eq!(std::mem::size_of::<ZstExtensions>(), 0);
        assert!(Header::<ZstExtensions>::has_zero_sized_extensions());

        #[allow(unused)]
        struct NonZstExtensions(u32);
        assert_ne!(std::mem::size_of::<NonZstExtensions>(), 0);
        assert!(!Header::<NonZstExtensions>::has_zero_sized_extensions());
    }
}