1use crate::{
4 Heightable,
5 aggregation::scheme,
6 types::{Epoch, Height},
7};
8use bytes::{Buf, BufMut, Bytes};
9use commonware_codec::{Encode, EncodeSize, Error as CodecError, Read, ReadExt, Write};
10use commonware_cryptography::{
11 Digest,
12 certificate::{AssemblyError, Attestation, Namespace as CertificateNamespace, Scheme, Subject},
13};
14use commonware_parallel::Strategy;
15use commonware_utils::{channel::oneshot, iter::NonEmpty, union};
16use rand_core::CryptoRng;
17use std::hash::Hash;
18
19#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("Application verify error: {0}")]
25 AppProposeCanceled(oneshot::error::RecvError),
26
27 #[error("Epoch {0} has no validator {1}")]
30 UnknownValidator(Epoch, String),
31
32 #[error("Peer mismatch")]
35 PeerMismatch,
36
37 #[error("Invalid ack signature")]
40 InvalidAckSignature,
41
42 #[error("Invalid ack epoch {0} outside bounds {1} - {2}")]
45 AckEpochOutsideBounds(Epoch, Epoch, Epoch),
46 #[error("Non-useful ack height {0}")]
48 AckHeight(Height),
49 #[error("Invalid ack digest {0}")]
51 AckDigest(Height),
52 #[error("Duplicate ack from sender {0} for height {1}")]
54 AckDuplicate(String, Height),
55 #[error("Ack for height {0} already has been certified")]
57 AckCertified(Height),
58 #[error("Unknown epoch {0}")]
60 UnknownEpoch(Epoch),
61}
62
63impl Error {
64 pub const fn blockable(&self) -> bool {
66 matches!(self, Self::PeerMismatch | Self::InvalidAckSignature)
67 }
68}
69
70const ACK_SUFFIX: &[u8] = b"_AGG_ACK";
73
74#[inline]
79fn ack_namespace(namespace: &[u8]) -> Vec<u8> {
80 union(namespace, ACK_SUFFIX)
81}
82
83#[derive(Clone, Debug)]
88pub struct Namespace(Vec<u8>);
89
90impl CertificateNamespace for Namespace {
91 fn derive(namespace: &[u8]) -> Self {
92 Self(ack_namespace(namespace))
93 }
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, Hash)]
99pub struct Item<D: Digest> {
100 pub height: Height,
102 pub digest: D,
104}
105
106impl<D: Digest> Heightable for Item<D> {
107 fn height(&self) -> Height {
108 self.height
109 }
110}
111
112impl<D: Digest> Write for Item<D> {
113 fn write(&self, writer: &mut impl BufMut) {
114 self.height.write(writer);
115 self.digest.write(writer);
116 }
117}
118
119impl<D: Digest> Read for Item<D> {
120 type Cfg = ();
121
122 fn read_cfg(reader: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
123 let height = Height::read(reader)?;
124 let digest = D::read(reader)?;
125 Ok(Self { height, digest })
126 }
127}
128
129impl<D: Digest> EncodeSize for Item<D> {
130 fn encode_size(&self) -> usize {
131 self.height.encode_size() + self.digest.encode_size()
132 }
133}
134
135impl<D: Digest> Subject for &Item<D> {
138 type Namespace = Namespace;
139
140 fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8] {
141 &derived.0
142 }
143
144 fn message(&self) -> Bytes {
145 self.encode()
146 }
147}
148
149#[cfg(feature = "arbitrary")]
150impl<D: Digest> arbitrary::Arbitrary<'_> for Item<D>
151where
152 D: for<'a> arbitrary::Arbitrary<'a>,
153{
154 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
155 let height = u.arbitrary::<Height>()?;
156 let digest = u.arbitrary::<D>()?;
157 Ok(Self { height, digest })
158 }
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Hash)]
164pub struct Ack<S: Scheme, D: Digest> {
165 pub item: Item<D>,
167 pub epoch: Epoch,
172 pub attestation: Attestation<S>,
174}
175
176impl<S: Scheme, D: Digest> Ack<S, D> {
177 pub fn verify<R>(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool
182 where
183 R: CryptoRng,
184 S: scheme::Scheme<D>,
185 {
186 scheme.verify_attestation::<_, D>(rng, &self.item, &self.attestation, strategy)
187 }
188
189 pub fn sign(scheme: &S, epoch: Epoch, item: Item<D>) -> Option<Self>
199 where
200 S: scheme::Scheme<D>,
201 {
202 let attestation = scheme.sign::<D>(&item)?;
203 Some(Self {
204 item,
205 epoch,
206 attestation,
207 })
208 }
209}
210
211impl<S: Scheme, D: Digest> Write for Ack<S, D> {
212 fn write(&self, writer: &mut impl BufMut) {
213 self.item.write(writer);
214 self.epoch.write(writer);
215 self.attestation.write(writer);
216 }
217}
218
219impl<S: Scheme, D: Digest> Read for Ack<S, D> {
220 type Cfg = ();
221
222 fn read_cfg(reader: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
223 let item = Item::read(reader)?;
224 let epoch = Epoch::read(reader)?;
225 let attestation = Attestation::read(reader)?;
226 Ok(Self {
227 item,
228 epoch,
229 attestation,
230 })
231 }
232}
233
234impl<S: Scheme, D: Digest> EncodeSize for Ack<S, D> {
235 fn encode_size(&self) -> usize {
236 self.item.encode_size() + self.epoch.encode_size() + self.attestation.encode_size()
237 }
238}
239
240#[cfg(feature = "arbitrary")]
241impl<S: Scheme, D: Digest> arbitrary::Arbitrary<'_> for Ack<S, D>
242where
243 S::Signature: for<'a> arbitrary::Arbitrary<'a>,
244 D: for<'a> arbitrary::Arbitrary<'a>,
245{
246 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
247 let item = u.arbitrary::<Item<D>>()?;
248 let epoch = u.arbitrary::<Epoch>()?;
249 let attestation = Attestation::arbitrary(u)?;
250 Ok(Self {
251 item,
252 epoch,
253 attestation,
254 })
255 }
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Hash)]
261pub struct TipAck<S: Scheme, D: Digest> {
262 pub tip: Height,
264
265 pub ack: Ack<S, D>,
267}
268
269impl<S: Scheme, D: Digest> Write for TipAck<S, D> {
270 fn write(&self, writer: &mut impl BufMut) {
271 self.tip.write(writer);
272 self.ack.write(writer);
273 }
274}
275
276impl<S: Scheme, D: Digest> Read for TipAck<S, D> {
277 type Cfg = ();
278
279 fn read_cfg(reader: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
280 let tip = Height::read(reader)?;
281 let ack = Ack::read(reader)?;
282 Ok(Self { tip, ack })
283 }
284}
285
286impl<S: Scheme, D: Digest> EncodeSize for TipAck<S, D> {
287 fn encode_size(&self) -> usize {
288 self.tip.encode_size() + self.ack.encode_size()
289 }
290}
291
292#[cfg(feature = "arbitrary")]
293impl<S: Scheme, D: Digest> arbitrary::Arbitrary<'_> for TipAck<S, D>
294where
295 D: for<'a> arbitrary::Arbitrary<'a>,
296 Ack<S, D>: for<'a> arbitrary::Arbitrary<'a>,
297{
298 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
299 let tip = u.arbitrary::<Height>()?;
300 let ack = u.arbitrary::<Ack<S, D>>()?;
301 Ok(Self { tip, ack })
302 }
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Hash)]
307pub struct Certificate<S: Scheme, D: Digest> {
308 pub item: Item<D>,
310 pub certificate: S::Certificate,
312}
313
314impl<S: Scheme, D: Digest> Certificate<S, D> {
315 pub fn from_acks<'a, I>(
317 scheme: &S,
318 acks: NonEmpty<I>,
319 strategy: &impl Strategy,
320 ) -> Result<Self, AssemblyError>
321 where
322 S: scheme::Scheme<D>,
323 I: Iterator<Item = &'a Ack<S, D>> + Send,
324 {
325 let (first, acks) = acks.into_parts();
326 let item = first.item.clone();
327 let attestations = NonEmpty::new(
328 first.attestation.clone(),
329 acks.filter(|ack| ack.item == item)
330 .map(|ack| ack.attestation.clone()),
331 );
332 let certificate = scheme.assemble(attestations, strategy)?;
333
334 Ok(Self { item, certificate })
335 }
336
337 pub fn verify<R>(&self, rng: &mut R, scheme: &S, strategy: &impl Strategy) -> bool
339 where
340 R: CryptoRng,
341 S: scheme::Scheme<D>,
342 {
343 scheme.verify_certificate::<_, D>(rng, &self.item, &self.certificate, strategy)
344 }
345}
346
347impl<S: Scheme, D: Digest> Write for Certificate<S, D> {
348 fn write(&self, writer: &mut impl BufMut) {
349 self.item.write(writer);
350 self.certificate.write(writer);
351 }
352}
353
354impl<S: Scheme, D: Digest> Read for Certificate<S, D> {
355 type Cfg = <S::Certificate as Read>::Cfg;
356
357 fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
358 let item = Item::read(reader)?;
359 let certificate = S::Certificate::read_cfg(reader, cfg)?;
360 Ok(Self { item, certificate })
361 }
362}
363
364impl<S: Scheme, D: Digest> EncodeSize for Certificate<S, D> {
365 fn encode_size(&self) -> usize {
366 self.item.encode_size() + self.certificate.encode_size()
367 }
368}
369
370#[cfg(feature = "arbitrary")]
371impl<S: Scheme, D: Digest> arbitrary::Arbitrary<'_> for Certificate<S, D>
372where
373 D: for<'a> arbitrary::Arbitrary<'a>,
374 S::Certificate: for<'a> arbitrary::Arbitrary<'a>,
375{
376 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
377 let item = u.arbitrary::<Item<D>>()?;
378 let certificate = u.arbitrary::<S::Certificate>()?;
379 Ok(Self { item, certificate })
380 }
381}
382
383#[derive(Clone, Debug, PartialEq)]
387pub enum Activity<S: Scheme, D: Digest> {
388 Ack(Ack<S, D>),
390
391 Certified(Certificate<S, D>),
393
394 Tip(Height),
396}
397
398impl<S: Scheme, D: Digest> Write for Activity<S, D> {
399 fn write(&self, writer: &mut impl BufMut) {
400 match self {
401 Self::Ack(ack) => {
402 0u8.write(writer);
403 ack.write(writer);
404 }
405 Self::Certified(certificate) => {
406 1u8.write(writer);
407 certificate.write(writer);
408 }
409 Self::Tip(height) => {
410 2u8.write(writer);
411 height.write(writer);
412 }
413 }
414 }
415}
416
417impl<S: Scheme, D: Digest> Read for Activity<S, D> {
418 type Cfg = <S::Certificate as Read>::Cfg;
419
420 fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
421 match u8::read(reader)? {
422 0 => Ok(Self::Ack(Ack::read(reader)?)),
423 1 => Ok(Self::Certified(Certificate::read_cfg(reader, cfg)?)),
424 2 => Ok(Self::Tip(Height::read(reader)?)),
425 _ => Err(CodecError::Invalid(
426 "consensus::aggregation::Activity",
427 "Invalid type",
428 )),
429 }
430 }
431}
432
433impl<S: Scheme, D: Digest> EncodeSize for Activity<S, D> {
434 fn encode_size(&self) -> usize {
435 1 + match self {
436 Self::Ack(ack) => ack.encode_size(),
437 Self::Certified(certificate) => certificate.encode_size(),
438 Self::Tip(height) => height.encode_size(),
439 }
440 }
441}
442
443#[cfg(feature = "arbitrary")]
444impl<S: Scheme, D: Digest> arbitrary::Arbitrary<'_> for Activity<S, D>
445where
446 D: for<'a> arbitrary::Arbitrary<'a>,
447 Ack<S, D>: for<'a> arbitrary::Arbitrary<'a>,
448 Certificate<S, D>: for<'a> arbitrary::Arbitrary<'a>,
449{
450 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
451 let choice = u.int_in_range(0..=2)?;
452 match choice {
453 0 => Ok(Self::Ack(u.arbitrary::<Ack<S, D>>()?)),
454 1 => Ok(Self::Certified(u.arbitrary::<Certificate<S, D>>()?)),
455 2 => Ok(Self::Tip(u.arbitrary::<Height>()?)),
456 _ => unreachable!(),
457 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::aggregation::scheme::{
465 Scheme, bls12381_multisig, bls12381_threshold, ed25519, secp256r1,
466 };
467 use bytes::BytesMut;
468 use commonware_codec::{Decode, DecodeExt, Encode};
469 use commonware_cryptography::{
470 Hasher, Sha256,
471 bls12381::primitives::variant::{MinPk, MinSig},
472 certificate::mocks::Fixture,
473 };
474 use commonware_parallel::Sequential;
475 use commonware_utils::{N3f1, TestRng, non_empty, ordered::Quorum, test_rng};
476
477 const NAMESPACE: &[u8] = b"test";
478
479 type Sha256Digest = <Sha256 as Hasher>::Digest;
480
481 #[test]
482 fn test_ack_namespace() {
483 let namespace = b"test_namespace";
484 let expected = [namespace, ACK_SUFFIX].concat();
485 assert_eq!(ack_namespace(namespace), expected);
486 }
487
488 fn codec<S, F>(fixture: F)
489 where
490 S: Scheme<Sha256Digest>,
491 F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture<S>,
492 {
493 let mut rng = test_rng();
494 let fixture = fixture(&mut rng, NAMESPACE, 4);
495 let schemes = &fixture.schemes;
496 let item = Item {
497 height: Height::new(100),
498 digest: Sha256::hash(&[b"test_item"]),
499 };
500
501 let restored_item = Item::decode(item.encode()).unwrap();
503 assert_eq!(item, restored_item);
504
505 let ack = Ack::sign(&schemes[0], Epoch::new(1), item.clone()).unwrap();
507 let cfg = schemes[0].certificate_codec_config();
508 let encoded_ack = ack.encode();
509 let restored_ack: Ack<S, Sha256Digest> = Ack::decode(encoded_ack).unwrap();
510
511 assert_eq!(restored_ack.item, item);
513 assert_eq!(restored_ack.epoch, Epoch::new(1));
514 assert!(restored_ack.verify(&mut rng, &schemes[0], &Sequential));
515
516 let tip_ack = TipAck {
518 ack: ack.clone(),
519 tip: Height::new(42),
520 };
521 let encoded_tip_ack = tip_ack.encode();
522 let restored_tip_ack: TipAck<S, Sha256Digest> = TipAck::decode(encoded_tip_ack).unwrap();
523 assert_eq!(restored_tip_ack.tip, Height::new(42));
524 assert_eq!(restored_tip_ack.ack.item, item);
525 assert_eq!(restored_tip_ack.ack.epoch, Epoch::new(1));
526
527 let activity_ack = Activity::Ack(ack);
529 let encoded_activity = activity_ack.encode();
530 let restored_activity_ack: Activity<S, Sha256Digest> =
531 Activity::decode_cfg(encoded_activity, &cfg).unwrap();
532 if let Activity::Ack(restored) = restored_activity_ack {
533 assert_eq!(restored.item, item);
534 assert_eq!(restored.epoch, Epoch::new(1));
535 } else {
536 panic!("Expected Activity::Ack");
537 }
538
539 let expected = schemes[0].participants().quorum::<N3f1>();
542 let expected_count = usize::try_from(expected).expect("quorum exceeds usize::MAX");
543 let acks: Vec<_> = schemes
544 .iter()
545 .take(expected_count)
546 .filter_map(|scheme| Ack::sign(scheme, Epoch::new(1), item.clone()))
547 .collect();
548
549 let insufficient = &acks[..acks.len() - 1];
551 let found = u32::try_from(insufficient.len()).expect("ack count exceeds u32::MAX");
552 assert!(matches!(
553 Certificate::from_acks(
554 &schemes[0],
555 non_empty![@insufficient],
556 &Sequential,
557 ),
558 Err(AssemblyError::InsufficientAttestations(
559 actual_expected,
560 actual_found
561 )) if actual_expected == expected && actual_found == found
562 ));
563
564 let certificate =
565 Certificate::from_acks(&schemes[0], non_empty![@acks.iter()], &Sequential).unwrap();
566 assert!(certificate.verify(&mut rng, &schemes[0], &Sequential));
567
568 let activity_certified = Activity::Certified(certificate.clone());
569 let encoded_certified = activity_certified.encode();
570 let restored_activity_certified: Activity<S, Sha256Digest> =
571 Activity::decode_cfg(encoded_certified, &cfg).unwrap();
572 if let Activity::Certified(restored) = restored_activity_certified {
573 assert_eq!(restored.item, item);
574 assert!(restored.verify(&mut rng, &schemes[0], &Sequential));
575 } else {
576 panic!("Expected Activity::Certified");
577 }
578
579 let activity_tip: Activity<S, Sha256Digest> = Activity::Tip(Height::new(123));
581 let encoded_tip = activity_tip.encode();
582 let restored_activity_tip: Activity<S, Sha256Digest> =
583 Activity::decode_cfg(encoded_tip, &cfg).unwrap();
584 if let Activity::Tip(height) = restored_activity_tip {
585 assert_eq!(height, Height::new(123));
586 } else {
587 panic!("Expected Activity::Tip");
588 }
589 }
590
591 #[test]
592 fn test_codec() {
593 codec(ed25519::fixture);
594 codec(secp256r1::fixture);
595 codec(bls12381_multisig::fixture::<MinPk, _>);
596 codec(bls12381_multisig::fixture::<MinSig, _>);
597 codec(bls12381_threshold::fixture::<MinPk, _>);
598 codec(bls12381_threshold::fixture::<MinSig, _>);
599 }
600
601 fn activity_invalid_enum<S, F>(fixture: F)
602 where
603 S: Scheme<Sha256Digest>,
604 F: FnOnce(&mut TestRng, &[u8], u32) -> Fixture<S>,
605 {
606 let fixture = fixture(&mut test_rng(), NAMESPACE, 4);
607 let mut buf = BytesMut::new();
608 3u8.write(&mut buf); let cfg = fixture.schemes[0].certificate_codec_config();
611 let result = Activity::<S, Sha256Digest>::read_cfg(&mut &buf[..], &cfg);
612 assert!(matches!(
613 result,
614 Err(CodecError::Invalid(
615 "consensus::aggregation::Activity",
616 "Invalid type"
617 ))
618 ));
619 }
620
621 #[test]
622 fn test_activity_invalid_enum() {
623 activity_invalid_enum(ed25519::fixture);
624 activity_invalid_enum(secp256r1::fixture);
625 activity_invalid_enum(bls12381_multisig::fixture::<MinPk, _>);
626 activity_invalid_enum(bls12381_multisig::fixture::<MinSig, _>);
627 activity_invalid_enum(bls12381_threshold::fixture::<MinPk, _>);
628 activity_invalid_enum(bls12381_threshold::fixture::<MinSig, _>);
629 }
630
631 #[cfg(feature = "arbitrary")]
632 mod conformance {
633 use super::*;
634 use crate::aggregation::scheme::bls12381_threshold;
635 use commonware_codec::conformance::CodecConformance;
636 use commonware_cryptography::{ed25519::PublicKey, sha256::Digest as Sha256Digest};
637
638 type Scheme = bls12381_threshold::Scheme<PublicKey, MinSig>;
639
640 commonware_conformance::conformance_tests! {
641 CodecConformance<Item<Sha256Digest>>,
642 CodecConformance<Ack<Scheme, Sha256Digest>>,
643 CodecConformance<TipAck<Scheme, Sha256Digest>>,
644 CodecConformance<Certificate<Scheme, Sha256Digest>>,
645 CodecConformance<Activity<Scheme, Sha256Digest>>,
646 }
647 }
648}