1use std::fmt;
2
3use minicbor::{Decoder, Encoder};
4
5use crate::{AuthorId, CapabilityId, CollectionId, CommitId, DomainId, SchemaId, TransactionId};
6
7const MAGIC: &str = "iroh-db";
8const FORMAT_MAJOR: u16 = 1;
9const FORMAT_MINOR: u16 = 0;
10const ENVELOPE_KIND: u8 = 2;
11const BODY_KIND: u8 = 3;
12const MAX_DEPENDENCIES: usize = 4_096;
13const MAX_SCHEMAS: usize = 4_096;
14const MAX_OPERATIONS: usize = 65_536;
15const MAX_RECORD_ID: usize = 1_024;
16const MAX_OPERATION_PAYLOAD: usize = 16 * 1024 * 1024;
17const MAX_CIPHERTEXT: usize = 64 * 1024 * 1024;
18
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
21pub struct CommitSignature([u8; 64]);
22
23impl CommitSignature {
24 pub const fn from_bytes(bytes: [u8; 64]) -> Self {
26 Self(bytes)
27 }
28
29 pub const fn as_bytes(&self) -> &[u8; 64] {
31 &self.0
32 }
33
34 pub const fn to_bytes(self) -> [u8; 64] {
36 self.0
37 }
38}
39
40impl fmt::Debug for CommitSignature {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str("CommitSignature([redacted])")
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct CommitHeader {
49 domain_id: DomainId,
50 epoch: u64,
51 author: AuthorId,
52 author_sequence: u64,
53 dependencies: Vec<CommitId>,
54 capability_id: CapabilityId,
55 schema_ids: Vec<SchemaId>,
56 nonce: [u8; 24],
57}
58
59impl CommitHeader {
60 #[allow(clippy::too_many_arguments)]
62 pub fn new(
63 domain_id: DomainId,
64 epoch: u64,
65 author: AuthorId,
66 author_sequence: u64,
67 mut dependencies: Vec<CommitId>,
68 capability_id: CapabilityId,
69 mut schema_ids: Vec<SchemaId>,
70 nonce: [u8; 24],
71 ) -> Result<Self, CommitCodecError> {
72 if dependencies.len() > MAX_DEPENDENCIES {
73 return Err(CommitCodecError::TooManyDependencies(dependencies.len()));
74 }
75 if schema_ids.len() > MAX_SCHEMAS {
76 return Err(CommitCodecError::TooManySchemas(schema_ids.len()));
77 }
78 dependencies.sort_unstable();
79 dependencies.dedup();
80 schema_ids.sort_unstable();
81 schema_ids.dedup();
82 Ok(Self {
83 domain_id,
84 epoch,
85 author,
86 author_sequence,
87 dependencies,
88 capability_id,
89 schema_ids,
90 nonce,
91 })
92 }
93
94 pub const fn domain_id(&self) -> DomainId {
96 self.domain_id
97 }
98
99 pub const fn epoch(&self) -> u64 {
101 self.epoch
102 }
103
104 pub const fn author(&self) -> AuthorId {
106 self.author
107 }
108
109 pub const fn author_sequence(&self) -> u64 {
111 self.author_sequence
112 }
113
114 pub fn dependencies(&self) -> &[CommitId] {
116 &self.dependencies
117 }
118
119 pub const fn capability_id(&self) -> CapabilityId {
121 self.capability_id
122 }
123
124 pub fn schema_ids(&self) -> &[SchemaId] {
126 &self.schema_ids
127 }
128
129 pub const fn nonce(&self) -> &[u8; 24] {
131 &self.nonce
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct CommitEnvelope {
138 header: CommitHeader,
139 ciphertext: Vec<u8>,
140 signature: CommitSignature,
141}
142
143impl CommitEnvelope {
144 pub fn new(
146 header: CommitHeader,
147 ciphertext: Vec<u8>,
148 signature: CommitSignature,
149 ) -> Result<Self, CommitCodecError> {
150 if ciphertext.len() > MAX_CIPHERTEXT {
151 return Err(CommitCodecError::CiphertextTooLarge(ciphertext.len()));
152 }
153 Ok(Self {
154 header,
155 ciphertext,
156 signature,
157 })
158 }
159
160 pub const fn header(&self) -> &CommitHeader {
162 &self.header
163 }
164
165 pub fn ciphertext(&self) -> &[u8] {
167 &self.ciphertext
168 }
169
170 pub const fn signature(&self) -> CommitSignature {
172 self.signature
173 }
174
175 pub fn signing_bytes(&self) -> Vec<u8> {
177 Self::signing_bytes_for(&self.header, &self.ciphertext)
178 .expect("an existing envelope is already size-validated")
179 }
180
181 pub fn signing_bytes_for(
183 header: &CommitHeader,
184 ciphertext: &[u8],
185 ) -> Result<Vec<u8>, CommitCodecError> {
186 if ciphertext.len() > MAX_CIPHERTEXT {
187 return Err(CommitCodecError::CiphertextTooLarge(ciphertext.len()));
188 }
189 Ok(encode_envelope(header, ciphertext, None))
190 }
191
192 pub fn encode_canonical(&self) -> Vec<u8> {
194 encode_envelope(&self.header, &self.ciphertext, Some(self.signature))
195 }
196
197 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, CommitCodecError> {
199 let envelope = decode_envelope(bytes)?;
200 if envelope.encode_canonical() != bytes {
201 return Err(CommitCodecError::NonCanonicalEncoding);
202 }
203 Ok(envelope)
204 }
205
206 pub fn commit_id(&self) -> CommitId {
208 CommitId::from_bytes(*blake3::hash(&self.encode_canonical()).as_bytes())
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct CommitBody {
215 transaction_id: TransactionId,
216 operations: Vec<Operation>,
217}
218
219impl CommitBody {
220 pub fn new(
222 transaction_id: TransactionId,
223 operations: Vec<Operation>,
224 ) -> Result<Self, CommitCodecError> {
225 if operations.len() > MAX_OPERATIONS {
226 return Err(CommitCodecError::TooManyOperations(operations.len()));
227 }
228 Ok(Self {
229 transaction_id,
230 operations,
231 })
232 }
233
234 pub const fn transaction_id(&self) -> TransactionId {
236 self.transaction_id
237 }
238
239 pub fn operations(&self) -> &[Operation] {
241 &self.operations
242 }
243
244 pub fn encode_canonical(&self) -> Vec<u8> {
246 let mut encoder = Encoder::new(Vec::new());
247 encode_body(&mut encoder, self).expect("encoding into Vec cannot fail");
248 encoder.into_writer()
249 }
250
251 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, CommitCodecError> {
253 let body = decode_body(bytes)?;
254 if body.encode_canonical() != bytes {
255 return Err(CommitCodecError::NonCanonicalEncoding);
256 }
257 Ok(body)
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263#[repr(u8)]
264pub enum OperationKind {
265 RecordCreate = 1,
267 RecordDelete = 2,
269 LwwAssign = 3,
271 OrSetAdd = 4,
273 OrSetRemove = 5,
275 GrowOnlySetAdd = 6,
277 CounterIncrement = 7,
279 CounterDecrement = 8,
281 MultiValueAssign = 9,
283 ImmutableSet = 10,
285 OrderedListInsert = 11,
287 OrderedListRemove = 12,
289 FieldStateMerge = 13,
291 SchemaStage = 14,
293 SchemaActivate = 15,
295}
296
297impl TryFrom<u8> for OperationKind {
298 type Error = CommitCodecError;
299
300 fn try_from(value: u8) -> Result<Self, Self::Error> {
301 match value {
302 1 => Ok(Self::RecordCreate),
303 2 => Ok(Self::RecordDelete),
304 3 => Ok(Self::LwwAssign),
305 4 => Ok(Self::OrSetAdd),
306 5 => Ok(Self::OrSetRemove),
307 6 => Ok(Self::GrowOnlySetAdd),
308 7 => Ok(Self::CounterIncrement),
309 8 => Ok(Self::CounterDecrement),
310 9 => Ok(Self::MultiValueAssign),
311 10 => Ok(Self::ImmutableSet),
312 11 => Ok(Self::OrderedListInsert),
313 12 => Ok(Self::OrderedListRemove),
314 13 => Ok(Self::FieldStateMerge),
315 14 => Ok(Self::SchemaStage),
316 15 => Ok(Self::SchemaActivate),
317 value => Err(CommitCodecError::UnknownOperationKind(value)),
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct Operation {
325 collection_id: CollectionId,
326 record_id: Vec<u8>,
327 field_id: u32,
328 kind: OperationKind,
329 payload: Vec<u8>,
330}
331
332impl Operation {
333 pub fn new(
335 collection_id: CollectionId,
336 record_id: Vec<u8>,
337 field_id: u32,
338 kind: OperationKind,
339 payload: Vec<u8>,
340 ) -> Result<Self, CommitCodecError> {
341 if record_id.len() > MAX_RECORD_ID {
342 return Err(CommitCodecError::RecordIdTooLarge(record_id.len()));
343 }
344 if payload.len() > MAX_OPERATION_PAYLOAD {
345 return Err(CommitCodecError::OperationPayloadTooLarge(payload.len()));
346 }
347 Ok(Self {
348 collection_id,
349 record_id,
350 field_id,
351 kind,
352 payload,
353 })
354 }
355
356 pub const fn collection_id(&self) -> CollectionId {
358 self.collection_id
359 }
360
361 pub fn record_id(&self) -> &[u8] {
363 &self.record_id
364 }
365
366 pub const fn field_id(&self) -> u32 {
368 self.field_id
369 }
370
371 pub const fn kind(&self) -> OperationKind {
373 self.kind
374 }
375
376 pub fn payload(&self) -> &[u8] {
378 &self.payload
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
384pub enum CommitCodecError {
385 #[error("too many commit dependencies: {0}")]
387 TooManyDependencies(usize),
388 #[error("too many schema references: {0}")]
390 TooManySchemas(usize),
391 #[error("too many operations: {0}")]
393 TooManyOperations(usize),
394 #[error("record id is too large: {0} bytes")]
396 RecordIdTooLarge(usize),
397 #[error("operation payload is too large: {0} bytes")]
399 OperationPayloadTooLarge(usize),
400 #[error("commit ciphertext is too large: {0} bytes")]
402 CiphertextTooLarge(usize),
403 #[error("unknown operation kind {0}")]
405 UnknownOperationKind(u8),
406 #[error("unsupported commit format")]
408 UnsupportedFormat,
409 #[error("invalid commit encoding: {0}")]
411 InvalidEncoding(String),
412 #[error("commit encoding is not canonical")]
414 NonCanonicalEncoding,
415}
416
417fn encode_envelope(
418 header: &CommitHeader,
419 ciphertext: &[u8],
420 signature: Option<CommitSignature>,
421) -> Vec<u8> {
422 let mut encoder = Encoder::new(Vec::new());
423 let field_count = if signature.is_some() { 14 } else { 13 };
424 encoder
425 .map(field_count)
426 .and_then(|encoder| encoder.u8(0))
427 .and_then(|encoder| encoder.str(MAGIC))
428 .and_then(|encoder| encoder.u8(1))
429 .and_then(|encoder| encoder.u16(FORMAT_MAJOR))
430 .and_then(|encoder| encoder.u8(2))
431 .and_then(|encoder| encoder.u16(FORMAT_MINOR))
432 .and_then(|encoder| encoder.u8(3))
433 .and_then(|encoder| encoder.u8(ENVELOPE_KIND))
434 .and_then(|encoder| encoder.u8(4))
435 .and_then(|encoder| encoder.bytes(header.domain_id().as_bytes()))
436 .and_then(|encoder| encoder.u8(5))
437 .and_then(|encoder| encoder.u64(header.epoch()))
438 .and_then(|encoder| encoder.u8(6))
439 .and_then(|encoder| encoder.bytes(header.author().as_bytes()))
440 .and_then(|encoder| encoder.u8(7))
441 .and_then(|encoder| encoder.u64(header.author_sequence()))
442 .expect("encoding into Vec cannot fail");
443 encoder
444 .u8(8)
445 .and_then(|encoder| encoder.array(header.dependencies().len() as u64))
446 .expect("encoding into Vec cannot fail");
447 for dependency in header.dependencies() {
448 encoder
449 .bytes(dependency.as_bytes())
450 .expect("encoding into Vec cannot fail");
451 }
452 encoder
453 .u8(9)
454 .and_then(|encoder| encoder.bytes(header.capability_id().as_bytes()))
455 .and_then(|encoder| encoder.u8(10))
456 .and_then(|encoder| encoder.array(header.schema_ids().len() as u64))
457 .expect("encoding into Vec cannot fail");
458 for schema_id in header.schema_ids() {
459 encoder
460 .bytes(schema_id.as_bytes())
461 .expect("encoding into Vec cannot fail");
462 }
463 encoder
464 .u8(11)
465 .and_then(|encoder| encoder.bytes(header.nonce()))
466 .and_then(|encoder| encoder.u8(12))
467 .and_then(|encoder| encoder.bytes(ciphertext))
468 .expect("encoding into Vec cannot fail");
469 if let Some(signature) = signature {
470 encoder
471 .u8(13)
472 .and_then(|encoder| encoder.bytes(signature.as_bytes()))
473 .expect("encoding into Vec cannot fail");
474 }
475 encoder.into_writer()
476}
477
478fn encode_body(
479 encoder: &mut Encoder<Vec<u8>>,
480 body: &CommitBody,
481) -> Result<(), minicbor::encode::Error<std::convert::Infallible>> {
482 encoder.map(6)?;
483 encoder.u8(0)?.str(MAGIC)?;
484 encoder.u8(1)?.u16(FORMAT_MAJOR)?;
485 encoder.u8(2)?.u16(FORMAT_MINOR)?;
486 encoder.u8(3)?.u8(BODY_KIND)?;
487 encoder.u8(4)?.bytes(body.transaction_id().as_bytes())?;
488 encoder.u8(5)?.array(body.operations().len() as u64)?;
489 for operation in body.operations() {
490 encoder.map(5)?;
491 encoder.u8(0)?.bytes(operation.collection_id().as_bytes())?;
492 encoder.u8(1)?.bytes(operation.record_id())?;
493 encoder.u8(2)?.u32(operation.field_id())?;
494 encoder.u8(3)?.u8(operation.kind() as u8)?;
495 encoder.u8(4)?.bytes(operation.payload())?;
496 }
497 Ok(())
498}
499
500fn decode_envelope(bytes: &[u8]) -> Result<CommitEnvelope, CommitCodecError> {
501 let mut decoder = Decoder::new(bytes);
502 expect_map_len(&mut decoder, 14)?;
503 decode_preamble(&mut decoder, ENVELOPE_KIND)?;
504 expect_key(&mut decoder, 4)?;
505 let domain_id = DomainId::from_bytes(read_fixed(&mut decoder, "domain id")?);
506 expect_key(&mut decoder, 5)?;
507 let epoch = decoder.u64().map_err(decode_error)?;
508 expect_key(&mut decoder, 6)?;
509 let author = AuthorId::from_bytes(read_fixed(&mut decoder, "author")?);
510 expect_key(&mut decoder, 7)?;
511 let author_sequence = decoder.u64().map_err(decode_error)?;
512 expect_key(&mut decoder, 8)?;
513 let dependency_count = read_array_len(&mut decoder, MAX_DEPENDENCIES, "dependencies")?;
514 let mut dependencies = Vec::with_capacity(dependency_count);
515 for _ in 0..dependency_count {
516 dependencies.push(CommitId::from_bytes(read_fixed(
517 &mut decoder,
518 "dependency",
519 )?));
520 }
521 expect_key(&mut decoder, 9)?;
522 let capability_id = CapabilityId::from_bytes(read_fixed(&mut decoder, "capability id")?);
523 expect_key(&mut decoder, 10)?;
524 let schema_count = read_array_len(&mut decoder, MAX_SCHEMAS, "schema ids")?;
525 let mut schema_ids = Vec::with_capacity(schema_count);
526 for _ in 0..schema_count {
527 schema_ids.push(SchemaId::from_bytes(read_fixed(&mut decoder, "schema id")?));
528 }
529 expect_key(&mut decoder, 11)?;
530 let nonce = read_fixed(&mut decoder, "nonce")?;
531 expect_key(&mut decoder, 12)?;
532 let ciphertext = read_bounded_bytes(&mut decoder, MAX_CIPHERTEXT, "ciphertext")?;
533 expect_key(&mut decoder, 13)?;
534 let signature = CommitSignature::from_bytes(read_fixed(&mut decoder, "signature")?);
535 if decoder.position() != bytes.len() {
536 return Err(CommitCodecError::InvalidEncoding("trailing data".into()));
537 }
538 let header = CommitHeader::new(
539 domain_id,
540 epoch,
541 author,
542 author_sequence,
543 dependencies,
544 capability_id,
545 schema_ids,
546 nonce,
547 )?;
548 CommitEnvelope::new(header, ciphertext, signature)
549}
550
551fn decode_body(bytes: &[u8]) -> Result<CommitBody, CommitCodecError> {
552 let mut decoder = Decoder::new(bytes);
553 expect_map_len(&mut decoder, 6)?;
554 decode_preamble(&mut decoder, BODY_KIND)?;
555 expect_key(&mut decoder, 4)?;
556 let transaction_id = TransactionId::from_bytes(read_fixed(&mut decoder, "transaction id")?);
557 expect_key(&mut decoder, 5)?;
558 let operation_count = read_array_len(&mut decoder, MAX_OPERATIONS, "operations")?;
559 let mut operations = Vec::with_capacity(operation_count);
560 for _ in 0..operation_count {
561 expect_map_len(&mut decoder, 5)?;
562 expect_key(&mut decoder, 0)?;
563 let collection_id = CollectionId::from_bytes(read_fixed(&mut decoder, "collection id")?);
564 expect_key(&mut decoder, 1)?;
565 let record_id = read_bounded_bytes(&mut decoder, MAX_RECORD_ID, "record id")?;
566 expect_key(&mut decoder, 2)?;
567 let field_id = decoder.u32().map_err(decode_error)?;
568 expect_key(&mut decoder, 3)?;
569 let kind = OperationKind::try_from(decoder.u8().map_err(decode_error)?)?;
570 expect_key(&mut decoder, 4)?;
571 let payload = read_bounded_bytes(&mut decoder, MAX_OPERATION_PAYLOAD, "operation payload")?;
572 operations.push(Operation::new(
573 collection_id,
574 record_id,
575 field_id,
576 kind,
577 payload,
578 )?);
579 }
580 if decoder.position() != bytes.len() {
581 return Err(CommitCodecError::InvalidEncoding("trailing data".into()));
582 }
583 CommitBody::new(transaction_id, operations)
584}
585
586fn decode_preamble(decoder: &mut Decoder<'_>, kind: u8) -> Result<(), CommitCodecError> {
587 expect_key(decoder, 0)?;
588 if decoder.str().map_err(decode_error)? != MAGIC {
589 return Err(CommitCodecError::UnsupportedFormat);
590 }
591 expect_key(decoder, 1)?;
592 if decoder.u16().map_err(decode_error)? != FORMAT_MAJOR {
593 return Err(CommitCodecError::UnsupportedFormat);
594 }
595 expect_key(decoder, 2)?;
596 if decoder.u16().map_err(decode_error)? != FORMAT_MINOR {
597 return Err(CommitCodecError::UnsupportedFormat);
598 }
599 expect_key(decoder, 3)?;
600 if decoder.u8().map_err(decode_error)? != kind {
601 return Err(CommitCodecError::UnsupportedFormat);
602 }
603 Ok(())
604}
605
606fn expect_map_len(decoder: &mut Decoder<'_>, expected: u64) -> Result<(), CommitCodecError> {
607 let actual = decoder
608 .map()
609 .map_err(decode_error)?
610 .ok_or_else(|| CommitCodecError::InvalidEncoding("indefinite map".into()))?;
611 if actual != expected {
612 return Err(CommitCodecError::InvalidEncoding(format!(
613 "expected map length {expected}, got {actual}"
614 )));
615 }
616 Ok(())
617}
618
619fn expect_key(decoder: &mut Decoder<'_>, expected: u8) -> Result<(), CommitCodecError> {
620 let actual = decoder.u8().map_err(decode_error)?;
621 if actual != expected {
622 return Err(CommitCodecError::InvalidEncoding(format!(
623 "expected key {expected}, got {actual}"
624 )));
625 }
626 Ok(())
627}
628
629fn read_array_len(
630 decoder: &mut Decoder<'_>,
631 maximum: usize,
632 name: &str,
633) -> Result<usize, CommitCodecError> {
634 let count = decoder
635 .array()
636 .map_err(decode_error)?
637 .ok_or_else(|| CommitCodecError::InvalidEncoding(format!("indefinite {name} array")))?;
638 let count = usize::try_from(count)
639 .map_err(|_| CommitCodecError::InvalidEncoding(format!("{name} count overflow")))?;
640 if count > maximum {
641 return Err(CommitCodecError::InvalidEncoding(format!(
642 "{name} count exceeds {maximum}"
643 )));
644 }
645 Ok(count)
646}
647
648fn read_bounded_bytes(
649 decoder: &mut Decoder<'_>,
650 maximum: usize,
651 name: &str,
652) -> Result<Vec<u8>, CommitCodecError> {
653 let bytes = decoder.bytes().map_err(decode_error)?;
654 if bytes.len() > maximum {
655 return Err(CommitCodecError::InvalidEncoding(format!(
656 "{name} exceeds {maximum} bytes"
657 )));
658 }
659 Ok(bytes.to_vec())
660}
661
662fn read_fixed<const LENGTH: usize>(
663 decoder: &mut Decoder<'_>,
664 name: &str,
665) -> Result<[u8; LENGTH], CommitCodecError> {
666 let bytes = decoder.bytes().map_err(decode_error)?;
667 bytes
668 .try_into()
669 .map_err(|_| CommitCodecError::InvalidEncoding(format!("{name} must be {LENGTH} bytes")))
670}
671
672#[allow(clippy::needless_pass_by_value)]
674fn decode_error(error: minicbor::decode::Error) -> CommitCodecError {
675 CommitCodecError::InvalidEncoding(error.to_string())
676}