Skip to main content

commonware_consensus/aggregation/
types.rs

1//! Types used in [aggregation](super).
2
3use 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/// Error that may be encountered when interacting with `aggregation`.
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22    // Proposal Errors
23    /// The proposal was canceled by the application
24    #[error("Application verify error: {0}")]
25    AppProposeCanceled(oneshot::error::RecvError),
26
27    // Epoch Errors
28    /// The specified validator is not a participant in the epoch
29    #[error("Epoch {0} has no validator {1}")]
30    UnknownValidator(Epoch, String),
31
32    // Peer Errors
33    /// The sender's public key doesn't match the expected key
34    #[error("Peer mismatch")]
35    PeerMismatch,
36
37    // Signature Errors
38    /// The acknowledgment signature is invalid
39    #[error("Invalid ack signature")]
40    InvalidAckSignature,
41
42    // Ignorable Message Errors
43    /// The acknowledgment's epoch is outside the accepted bounds
44    #[error("Invalid ack epoch {0} outside bounds {1} - {2}")]
45    AckEpochOutsideBounds(Epoch, Epoch, Epoch),
46    /// The acknowledgment's height is outside the accepted bounds
47    #[error("Non-useful ack height {0}")]
48    AckHeight(Height),
49    /// The acknowledgment's digest is incorrect
50    #[error("Invalid ack digest {0}")]
51    AckDigest(Height),
52    /// Duplicate acknowledgment for the same height
53    #[error("Duplicate ack from sender {0} for height {1}")]
54    AckDuplicate(String, Height),
55    /// The acknowledgement is for a height that already has a certificate
56    #[error("Ack for height {0} already has been certified")]
57    AckCertified(Height),
58    /// The epoch is unknown
59    #[error("Unknown epoch {0}")]
60    UnknownEpoch(Epoch),
61}
62
63impl Error {
64    /// Returns true if the error represents a blockable offense by a peer.
65    pub const fn blockable(&self) -> bool {
66        matches!(self, Self::PeerMismatch | Self::InvalidAckSignature)
67    }
68}
69
70/// Suffix used to identify an acknowledgment (ack) namespace for domain separation.
71/// Used when signing and verifying acks to prevent signature reuse across different message types.
72const ACK_SUFFIX: &[u8] = b"_AGG_ACK";
73
74/// Returns a suffixed namespace for signing an ack.
75///
76/// This provides domain separation for signatures, preventing cross-protocol attacks
77/// by ensuring signatures for acks cannot be reused for other message types.
78#[inline]
79fn ack_namespace(namespace: &[u8]) -> Vec<u8> {
80    union(namespace, ACK_SUFFIX)
81}
82
83/// Namespace type for aggregation acknowledgments.
84///
85/// This type encapsulates the pre-computed namespace bytes used for signing and
86/// verifying acks.
87#[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/// Item represents a single element being aggregated in the protocol.
97/// Each item has a unique height and contains a digest that validators sign.
98#[derive(Clone, Debug, PartialEq, Eq, Hash)]
99pub struct Item<D: Digest> {
100    /// Sequential position of this item within the current epoch
101    pub height: Height,
102    /// Cryptographic digest of the data being aggregated
103    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
135/// The signed message covers only the height and digest, intentionally excluding the epoch.
136/// See the [module docs](super#epoch-independent-signatures).
137impl<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/// Acknowledgment (ack) represents a validator's vote on an item.
162/// Multiple acks can be recovered into a certificate for consensus.
163#[derive(Clone, Debug, PartialEq, Eq, Hash)]
164pub struct Ack<S: Scheme, D: Digest> {
165    /// The item being acknowledged
166    pub item: Item<D>,
167    /// The epoch in which this acknowledgment was created.
168    ///
169    /// Not part of the signed message: it selects the scheme used to verify the attestation and
170    /// assemble a certificate. See the [module docs](super#epoch-independent-signatures).
171    pub epoch: Epoch,
172    /// Scheme-specific attestation material
173    pub attestation: Attestation<S>,
174}
175
176impl<S: Scheme, D: Digest> Ack<S, D> {
177    /// Verifies the attestation on this acknowledgment.
178    ///
179    /// Returns `true` if the attestation is valid for the given namespace and public key.
180    /// Domain separation is automatically applied to prevent signature reuse.
181    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    /// Creates a new acknowledgment by signing an item with a validator's key.
190    ///
191    /// The signature uses domain separation to prevent cross-protocol attacks. The epoch is
192    /// carried in the ack but is not part of the signed message. See the
193    /// [module docs](super#epoch-independent-signatures).
194    ///
195    /// # Determinism
196    ///
197    /// Signatures produced by this function are deterministic and safe for consensus.
198    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/// Message exchanged between peers containing an acknowledgment and tip information.
259/// This combines a validator's vote with their view of consensus progress.
260#[derive(Clone, Debug, PartialEq, Eq, Hash)]
261pub struct TipAck<S: Scheme, D: Digest> {
262    /// The peer's local view of the tip (the lowest height that is not yet confirmed).
263    pub tip: Height,
264
265    /// The peer's acknowledgement (vote) for an item.
266    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/// A recovered certificate for some [Item].
306#[derive(Clone, Debug, PartialEq, Eq, Hash)]
307pub struct Certificate<S: Scheme, D: Digest> {
308    /// The item that was recovered.
309    pub item: Item<D>,
310    /// The recovered certificate.
311    pub certificate: S::Certificate,
312}
313
314impl<S: Scheme, D: Digest> Certificate<S, D> {
315    /// Builds a certificate from non-empty acknowledgements for the first observed item.
316    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    /// Verifies the recovered certificate for the item.
338    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/// Used as [Reporter::Activity](crate::Reporter::Activity) to report activities that occur during
384/// aggregation. Also used to journal events that are needed to initialize the aggregation engine
385/// when the node restarts.
386#[derive(Clone, Debug, PartialEq)]
387pub enum Activity<S: Scheme, D: Digest> {
388    /// Received an ack from a participant.
389    Ack(Ack<S, D>),
390
391    /// Certified an [Item].
392    Certified(Certificate<S, D>),
393
394    /// Moved the tip to a new height.
395    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        // Test Item codec
502        let restored_item = Item::decode(item.encode()).unwrap();
503        assert_eq!(item, restored_item);
504
505        // Test Ack creation and codec
506        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        // Verify the restored ack
512        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        // Test TipAck codec
517        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        // Test Activity codec - Ack variant
528        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        // Test Activity codec - Certified variant
540        // Collect enough acks for a certificate
541        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        // A non-empty sub-quorum still reports the exact shortfall.
550        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        // Test Activity codec - Tip variant
580        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); // Invalid discriminant
609
610        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}