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