p2panda_core/operation/any.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use crate::hash::{HASH_LEN, Hash};
4use crate::identity::{SIGNATURE_LEN, Signature, VERIFYING_KEY_LEN, VerifyingKey};
5use crate::logs::SeqNum;
6use crate::operation::HeaderError;
7use crate::operation::header::encode_header;
8use crate::operation::{Body, Header, PayloadSize, RawOperation, Version};
9use crate::traits::{Chain, Digest, Extensions, Offchain, Provenance};
10
11/// Combined [`AnyHeader`], [`Body`] and operation [`struct@Hash`] (Operation Id).
12///
13/// ## Extensions
14///
15/// `AnyOperation` does not know the concrete extensions type. On this level it is only concerned
16/// with the validity and integrity of the append-only log type itself which is enough for most
17/// low-level protocols, such as the sync protocol.
18///
19/// Applications usually want to attach custom extensions to the operation, if you need to know the
20/// type you can easily convert from `AnyOperation` to [`Operation`](crate::Operation) with an
21/// explicit `E` extensions type.
22#[derive(Clone, Debug)]
23pub struct AnyOperation {
24 pub hash: Hash,
25 pub header: AnyHeader,
26 pub body: Option<Body>,
27}
28
29impl Digest<Hash> for AnyOperation {
30 fn hash(&self) -> Hash {
31 self.hash
32 }
33}
34
35impl Provenance<VerifyingKey> for AnyOperation {
36 fn author(&self) -> VerifyingKey {
37 self.header.verifying_key
38 }
39
40 fn verify(&self) -> bool {
41 self.header.verify()
42 }
43}
44
45impl Chain<Hash> for AnyOperation {
46 fn backlink(&self) -> Option<Hash> {
47 self.header.backlink
48 }
49
50 fn seq_num(&self) -> SeqNum {
51 self.header.seq_num
52 }
53}
54
55impl Offchain<Hash> for AnyOperation {
56 fn payload(&self) -> Option<&Body> {
57 self.body.as_ref()
58 }
59
60 fn payload_hash(&self) -> Option<Hash> {
61 self.header.payload_hash
62 }
63
64 fn payload_size(&self) -> PayloadSize {
65 self.header.payload_size
66 }
67}
68
69impl TryFrom<RawOperation> for AnyOperation {
70 type Error = HeaderError;
71
72 fn try_from(bytes: RawOperation) -> Result<Self, Self::Error> {
73 let (header_bytes, body_bytes) = bytes;
74 let header: AnyHeader = AnyHeader::decode(&header_bytes)?;
75
76 Ok(AnyOperation {
77 hash: header.hash(),
78 header,
79 body: body_bytes.map(Body::from),
80 })
81 }
82}
83
84/// Header of a p2panda operation.
85///
86/// The header holds all metadata required to cryptographically secure and authenticate a message
87/// [`Body`] and it's custom extensions.
88///
89/// ## Extensions
90///
91/// `AnyHeader` does not know the concrete extensions type. On this level it is only concerned with
92/// the validity and integrity of the append-only log type itself which is enough for most low-level
93/// protocols, such as the sync protocol.
94///
95/// Applications usually want to attach custom extensions to the header, if you need to know the
96/// type you can easily convert from `AnyHeader` to [`Header`] with an explicit `E` extensions
97/// type.
98///
99/// ```rust
100/// # fn example() -> Result<(), p2panda_core::HeaderError> {
101/// use p2panda_core::{AnyHeader, Hash, Header, SigningKey};
102/// use serde::{Deserialize, Serialize};
103///
104/// #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105/// struct MyExtensions {
106/// dependencies: Vec<Hash>,
107/// }
108///
109/// let signing_key = SigningKey::generate();
110///
111/// // Create a Header with concrete extension type `MyExtensions`.
112/// let header = Header::builder()
113/// .build(&signing_key, MyExtensions {
114/// dependencies: vec![Hash::from([0; 32])],
115/// });
116///
117/// // Encode it to CBOR bytes, this is how we transmit operations over the network.
118/// let bytes = header.encode();
119///
120/// // Convert it to `AnyHeader` which doesn't know the extensions type.
121/// let any_header = AnyHeader::decode(&bytes)?;
122///
123/// // Bring it back to a concrete Header type with `MyExtensions`.
124/// let header_again = Header::try_from(any_header)?;
125/// assert_eq!(header, header_again);
126/// # Ok(())
127/// # }
128/// ```
129///
130/// Please note that at this stage we can only verify the integrity and authenticity of the attached
131/// extensions, we _don't know_ if the extensions themselves are valid. We can only find out if this
132/// is correct if we know the concrete E type.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct AnyHeader {
135 /// Operation format version, allowing backwards compatibility when specification changes.
136 pub version: Version,
137
138 /// Author of this operation.
139 pub verifying_key: VerifyingKey,
140
141 /// Signature by author over all fields in header, providing authenticity.
142 pub signature: Signature,
143
144 /// Number of bytes of the body of this operation, must be zero if no body is given.
145 pub payload_size: PayloadSize,
146
147 /// Hash of the body of this operation, must be included if payload_size is non-zero and
148 /// omitted otherwise.
149 ///
150 /// Keeping the hash here allows us to delete the payload (off-chain data) while retaining the
151 /// ability to check the signature of the header.
152 pub payload_hash: Option<Hash>,
153
154 /// Number of operations this author has published to this log, begins with 0 and is always
155 /// incremented by 1 with each new operation by the same author.
156 pub seq_num: SeqNum,
157
158 /// Hash of the previous operation of the same author and log. Can be omitted if first
159 /// operation in log.
160 pub backlink: Option<Hash>,
161
162 /// Size of header in encoded CBOR bytes.
163 pub(crate) size: u32,
164
165 /// BLAKE3 hash digest of header.
166 pub(crate) digest: Hash,
167
168 /// Custom additional data.
169 ///
170 /// We don't know the exact Rust type of the extensions here, only the AST representation of
171 /// CBOR. To decode the value to an extensions Rust type `E` use
172 /// `Header::<E>::try_from`(crate::Header::try_from).
173 pub(crate) extensions: Option<cbor_core::Value<'static>>,
174}
175
176impl AnyHeader {
177 /// Attempts decoding header from bytes.
178 ///
179 /// This fails if integrity checks failed or header formatting is invalid.
180 pub fn decode(bytes: &[u8]) -> Result<Self, HeaderError> {
181 // Attempt decoding bytes as CBOR.
182 //
183 // The bytes are decoded in a zero-copy manner, only reading from the given byte slice.
184 let cbor = {
185 let codec = cbor_core::DecodeOptions::new()
186 // Enforce a strict, canonical CBOR encoding, otherwise integrity checks would fail
187 // when decoding & encoding the headers again on our end. See `encode_header` for
188 // details.
189 .strictness(cbor_core::Strictness::STRICT)
190 // Make sure some attacks are mitigated and set rather low / pessimistic thresholds.
191 .recursion_limit(64)
192 .length_limit(512) // 0.5kb
193 .oom_mitigation(64);
194
195 codec.decode(bytes).map_err(HeaderError::DecodingHeader)?
196 };
197
198 // Validate each field in header based on p2panda specification and extract Rust types.
199 //
200 // Every header is a tuple (CBOR array). We iterate over each field and check if the
201 // expected CBOR and Rust type is given.
202 //
203 // The types are converted into owned objects (leaving the zero-copy nature of this process)
204 // and kept to allow further validation (log integrity) or conversion into the more
205 // specialised Header<E> type (where the Extensions are known).
206 //
207 // We don't keep the CBOR representation or bytes around anymore in the end (except of the
208 // decoded extensions) to not waste memory with duplicate representations of the same data.
209 let mut seq = cbor
210 .into_array()
211 .map_err(HeaderError::UnexpectedHeaderType)?;
212 let mut iter = seq.iter();
213
214 let version = {
215 let next = iter.next().ok_or(HeaderError::MissingField("version"))?;
216
217 Version::try_from(next)
218 .map_err(|err| HeaderError::UnexpectedFieldType(err, "version"))?
219 };
220
221 if version != 1 {
222 return Err(HeaderError::UnsupportedVersion(version, 1));
223 }
224
225 let verifying_key = {
226 let next = iter
227 .next()
228 .ok_or(HeaderError::MissingField("verifying_key"))?;
229
230 let bytes = next
231 .as_bytes()
232 .map_err(|err| HeaderError::UnexpectedFieldType(err, "verifying_key"))?;
233
234 let bytes: [u8; VERIFYING_KEY_LEN] = bytes.try_into().map_err(|_| {
235 HeaderError::InvalidBytesLen("verifying_key", VERIFYING_KEY_LEN, bytes.len())
236 })?;
237
238 VerifyingKey::from_bytes(&bytes).map_err(HeaderError::InvalidVerifyingKey)?
239 };
240
241 let signature = {
242 let next = iter.next().ok_or(HeaderError::MissingField("signature"))?;
243
244 let bytes = next
245 .as_bytes()
246 .map_err(|err| HeaderError::UnexpectedFieldType(err, "signature"))?;
247
248 let bytes: [u8; SIGNATURE_LEN] = bytes.try_into().map_err(|_| {
249 HeaderError::InvalidBytesLen("signature", SIGNATURE_LEN, bytes.len())
250 })?;
251
252 Signature::from(&bytes)
253 };
254
255 let payload_size = {
256 let next = iter
257 .next()
258 .ok_or(HeaderError::MissingField("payload_size"))?;
259
260 PayloadSize::try_from(next)
261 .map_err(|err| HeaderError::UnexpectedFieldType(err, "payload_size"))?
262 };
263
264 let payload_hash = if payload_size > 0 {
265 let next = iter
266 .next()
267 .ok_or(HeaderError::MissingField("payload_hash"))?;
268
269 let bytes = next
270 .as_bytes()
271 .map_err(|err| HeaderError::UnexpectedFieldType(err, "payload_hash"))?;
272
273 let bytes: [u8; HASH_LEN] = bytes
274 .try_into()
275 .map_err(|_| HeaderError::InvalidBytesLen("payload_hash", HASH_LEN, bytes.len()))?;
276
277 Some(Hash::from(bytes))
278 } else {
279 None
280 };
281
282 let seq_num = {
283 let next = iter.next().ok_or(HeaderError::MissingField("seq_num"))?;
284
285 SeqNum::try_from(next)
286 .map_err(|err| HeaderError::UnexpectedFieldType(err, "seq_num"))?
287 };
288
289 let backlink = if seq_num > 0 {
290 let next = iter.next().ok_or(HeaderError::MissingField("backlink"))?;
291
292 let bytes = next
293 .as_bytes()
294 .map_err(|err| HeaderError::UnexpectedFieldType(err, "backlink"))?;
295
296 let bytes: [u8; HASH_LEN] = bytes
297 .try_into()
298 .map_err(|_| HeaderError::InvalidBytesLen("backlink", HASH_LEN, bytes.len()))?;
299
300 Some(Hash::from(bytes))
301 } else {
302 None
303 };
304
305 // Extract extensions and keep them for later, in case we need to deserialize them into
306 // Header<E> in the future.
307 //
308 // AnyHeader doesn't know the Rust type for E, only it's "raw" CBOR representation. To use
309 // extensions properly with Rust types we eventually want to convert into the concrete E
310 // type.
311 //
312 // Please note that at this stage we _don't know_ if this header is valid with the
313 // extensions set. We can only find out if this is correct if we know the concrete E type
314 // (if it's a ZST then there should not be an extensions field).
315 let extensions = iter.next().map(|value| value.to_owned());
316
317 // If anything came after all expected fields, something is wrong.
318 if iter.next().is_some() {
319 return Err(HeaderError::ExcessiveFields);
320 }
321
322 // Verify signature.
323 //
324 // Extract signature from field position 2. It'll be removed from the CBOR value, so we can
325 // encode the bytes without it.
326 //
327 // [0] [1] [2]
328 // (version, verifying_key, signature, ..)
329 // =========
330 seq.remove(2);
331
332 let verify_bytes = cbor_core::Value::from(seq).encode();
333 if !verifying_key.verify(&verify_bytes, &signature) {
334 return Err(HeaderError::InvalidSignature);
335 }
336
337 // Calculate header size and generate hash digest.
338 //
339 // We keep these values around so if users of this object require the size or hash, it will
340 // not be re-computed again.
341 //
342 // Since we also have the bytes in our hands already we don't need to encode either.
343 let size = bytes.len() as u32;
344 let digest = Hash::digest(bytes);
345
346 Ok(Self {
347 version,
348 verifying_key,
349 signature,
350 payload_size,
351 payload_hash,
352 seq_num,
353 backlink,
354 size,
355 digest,
356 extensions,
357 })
358 }
359
360 /// Encodes header to byte-representation (CBOR).
361 pub fn encode(&self) -> Vec<u8> {
362 encode_header(
363 self.version,
364 self.verifying_key,
365 Some(&self.signature),
366 self.payload_size,
367 self.payload_hash,
368 self.seq_num,
369 self.backlink,
370 self.extensions.as_ref(),
371 )
372 }
373
374 /// BLAKE3 hash of the header bytes.
375 ///
376 /// This hash is used as the unique identifier of an operation, aka the Operation Id.
377 pub fn hash(&self) -> Hash {
378 self.digest
379 }
380
381 /// Size of header when encoded as CBOR bytes.
382 pub fn size(&self) -> u32 {
383 self.size
384 }
385}
386
387impl Digest<Hash> for AnyHeader {
388 fn hash(&self) -> Hash {
389 self.hash()
390 }
391}
392
393impl Provenance<VerifyingKey> for AnyHeader {
394 fn author(&self) -> VerifyingKey {
395 self.verifying_key
396 }
397
398 fn verify(&self) -> bool {
399 // Was checked during decoding.
400 true
401 }
402}
403
404impl Chain<Hash> for AnyHeader {
405 fn backlink(&self) -> Option<Hash> {
406 self.backlink
407 }
408
409 fn seq_num(&self) -> SeqNum {
410 self.seq_num
411 }
412}
413
414impl Offchain<Hash> for AnyHeader {
415 fn payload(&self) -> Option<&Body> {
416 None
417 }
418
419 fn payload_hash(&self) -> Option<Hash> {
420 self.payload_hash
421 }
422
423 fn payload_size(&self) -> PayloadSize {
424 self.payload_size
425 }
426}
427
428impl TryFrom<&[u8]> for AnyHeader {
429 type Error = HeaderError;
430
431 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
432 Self::decode(value)
433 }
434}
435
436impl TryFrom<Vec<u8>> for AnyHeader {
437 type Error = HeaderError;
438
439 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
440 Self::decode(&value)
441 }
442}
443
444impl<E> TryFrom<Header<E>> for AnyHeader
445where
446 E: Extensions,
447{
448 type Error = HeaderError;
449
450 fn try_from(value: Header<E>) -> Result<Self, Self::Error> {
451 let extensions = if !Header::<E>::has_zero_sized_extensions() {
452 Some(
453 cbor_core::Value::serialized(&value.extensions)
454 .map_err(HeaderError::EncodingExtensions)?,
455 )
456 } else {
457 None
458 };
459
460 Ok(AnyHeader {
461 version: value.version,
462 verifying_key: value.verifying_key,
463 signature: value.signature,
464 payload_size: value.payload_size,
465 payload_hash: value.payload_hash,
466 seq_num: value.seq_num,
467 backlink: value.backlink,
468 size: value.size,
469 digest: value.digest,
470 extensions,
471 })
472 }
473}