lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

evidence_manifest.rs (45307B)


      1 #![forbid(unsafe_code)]
      2 
      3 //! Immutable evidence inventory for one governed trade generation.
      4 //!
      5 //! The manifest commits to caller-supplied canonical record bytes through
      6 //! semantically distinct SHA-256 digest types. It does not retrieve, validate,
      7 //! persist, sign, or publish those records.
      8 
      9 use alloc::{boxed::Box, string::String, vec, vec::Vec};
     10 use core::{fmt, num::NonZeroU64};
     11 
     12 use radroots_event::id::{EventId, MutationId, TradeId};
     13 use sha2::{Digest as _, Sha256};
     14 
     15 #[cfg(test)]
     16 use crate::evidence::RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE;
     17 use crate::evidence::{
     18     RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT, RadrootsTradeEvidenceCoverageError,
     19     RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceScopePrerequisitesV1,
     20     RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceRequirementV1,
     21     RadrootsTradeEvidenceSourceResultV1, classify_trade_evidence_coverage_v1,
     22 };
     23 
     24 pub const RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID: &str =
     25     "radroots.trade.evidence-manifest.v1";
     26 pub const RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION: u16 = 1;
     27 pub const RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES: usize = 64;
     28 pub const RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS: usize = 65_536;
     29 pub const RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES: usize = 16 * 1024 * 1024;
     30 
     31 const MANIFEST_PREFIX: &[u8] = b"radroots.trade.evidence-manifest.v1\0";
     32 const MANIFEST_DIGEST_DOMAIN: &[u8] = b"radroots:trade-evidence-manifest-digest:v1\0";
     33 
     34 macro_rules! define_content_digest {
     35     ($name:ident) => {
     36         #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     37         pub struct $name([u8; 32]);
     38 
     39         impl $name {
     40             pub const fn from_bytes(bytes: [u8; 32]) -> Self {
     41                 Self(bytes)
     42             }
     43 
     44             pub fn sha256(bytes: &[u8]) -> Self {
     45                 Self(Sha256::digest(bytes).into())
     46             }
     47 
     48             pub const fn as_bytes(&self) -> &[u8; 32] {
     49                 &self.0
     50             }
     51 
     52             pub fn to_hex(&self) -> String {
     53                 hex::encode(self.0)
     54             }
     55         }
     56 
     57         impl fmt::Debug for $name {
     58             fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     59                 formatter.write_str(concat!(stringify!($name), "(<redacted>)"))
     60             }
     61         }
     62     };
     63 }
     64 
     65 define_content_digest!(RadrootsTradeEvidencePolicyDigestV1);
     66 define_content_digest!(RadrootsTradeEvidenceSourceResultDigestV1);
     67 define_content_digest!(RadrootsTradeSignedEventDigestV1);
     68 define_content_digest!(RadrootsTradeEvidenceProvenanceDigestV1);
     69 
     70 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     71 pub struct RadrootsTradeEvidenceManifestDigestV1([u8; 32]);
     72 
     73 impl RadrootsTradeEvidenceManifestDigestV1 {
     74     pub const fn from_bytes(bytes: [u8; 32]) -> Self {
     75         Self(bytes)
     76     }
     77 
     78     pub const fn as_bytes(&self) -> &[u8; 32] {
     79         &self.0
     80     }
     81 
     82     pub fn to_hex(&self) -> String {
     83         hex::encode(self.0)
     84     }
     85 }
     86 
     87 impl fmt::Debug for RadrootsTradeEvidenceManifestDigestV1 {
     88     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     89         formatter.write_str("RadrootsTradeEvidenceManifestDigestV1(<redacted>)")
     90     }
     91 }
     92 
     93 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
     94 pub struct RadrootsTradeEvidenceSourceIdV1(String);
     95 
     96 impl RadrootsTradeEvidenceSourceIdV1 {
     97     pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsTradeEvidenceManifestError> {
     98         let value = value.as_ref();
     99         if value.is_empty()
    100             || value.len() > RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES
    101             || !value.as_bytes()[0].is_ascii_lowercase()
    102             || value.bytes().any(|byte| {
    103                 !(byte.is_ascii_lowercase()
    104                     || byte.is_ascii_digit()
    105                     || byte == b'_'
    106                     || byte == b'-')
    107             })
    108         {
    109             return Err(RadrootsTradeEvidenceManifestError::InvalidSourceId);
    110         }
    111         Ok(Self(String::from(value)))
    112     }
    113 
    114     pub fn as_str(&self) -> &str {
    115         self.0.as_str()
    116     }
    117 }
    118 
    119 impl fmt::Debug for RadrootsTradeEvidenceSourceIdV1 {
    120     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    121         formatter.write_str("RadrootsTradeEvidenceSourceIdV1(<redacted>)")
    122     }
    123 }
    124 
    125 #[derive(Clone, PartialEq, Eq)]
    126 pub struct RadrootsTradeEvidenceManifestSourceResultV1 {
    127     source_id: RadrootsTradeEvidenceSourceIdV1,
    128     result: RadrootsTradeEvidenceSourceResultV1,
    129     result_digest: RadrootsTradeEvidenceSourceResultDigestV1,
    130 }
    131 
    132 impl RadrootsTradeEvidenceManifestSourceResultV1 {
    133     pub const fn new(
    134         source_id: RadrootsTradeEvidenceSourceIdV1,
    135         result: RadrootsTradeEvidenceSourceResultV1,
    136         result_digest: RadrootsTradeEvidenceSourceResultDigestV1,
    137     ) -> Self {
    138         Self {
    139             source_id,
    140             result,
    141             result_digest,
    142         }
    143     }
    144 
    145     pub const fn source_id(&self) -> &RadrootsTradeEvidenceSourceIdV1 {
    146         &self.source_id
    147     }
    148 
    149     pub const fn result(&self) -> RadrootsTradeEvidenceSourceResultV1 {
    150         self.result
    151     }
    152 
    153     pub const fn result_digest(&self) -> RadrootsTradeEvidenceSourceResultDigestV1 {
    154         self.result_digest
    155     }
    156 }
    157 
    158 impl fmt::Debug for RadrootsTradeEvidenceManifestSourceResultV1 {
    159     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    160         formatter.write_str("RadrootsTradeEvidenceManifestSourceResultV1(<redacted>)")
    161     }
    162 }
    163 
    164 #[derive(Clone, PartialEq, Eq)]
    165 pub struct RadrootsTradeEvidenceManifestObservationV1 {
    166     source_id: RadrootsTradeEvidenceSourceIdV1,
    167     mutation_id: MutationId,
    168     event_id: EventId,
    169     signed_event_digest: RadrootsTradeSignedEventDigestV1,
    170     provenance_digest: RadrootsTradeEvidenceProvenanceDigestV1,
    171 }
    172 
    173 impl RadrootsTradeEvidenceManifestObservationV1 {
    174     pub const fn new(
    175         source_id: RadrootsTradeEvidenceSourceIdV1,
    176         mutation_id: MutationId,
    177         event_id: EventId,
    178         signed_event_digest: RadrootsTradeSignedEventDigestV1,
    179         provenance_digest: RadrootsTradeEvidenceProvenanceDigestV1,
    180     ) -> Self {
    181         Self {
    182             source_id,
    183             mutation_id,
    184             event_id,
    185             signed_event_digest,
    186             provenance_digest,
    187         }
    188     }
    189 
    190     pub const fn source_id(&self) -> &RadrootsTradeEvidenceSourceIdV1 {
    191         &self.source_id
    192     }
    193 
    194     pub const fn mutation_id(&self) -> &MutationId {
    195         &self.mutation_id
    196     }
    197 
    198     pub const fn event_id(&self) -> &EventId {
    199         &self.event_id
    200     }
    201 
    202     pub const fn signed_event_digest(&self) -> RadrootsTradeSignedEventDigestV1 {
    203         self.signed_event_digest
    204     }
    205 
    206     pub const fn provenance_digest(&self) -> RadrootsTradeEvidenceProvenanceDigestV1 {
    207         self.provenance_digest
    208     }
    209 }
    210 
    211 impl fmt::Debug for RadrootsTradeEvidenceManifestObservationV1 {
    212     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    213         formatter.write_str("RadrootsTradeEvidenceManifestObservationV1(<redacted>)")
    214     }
    215 }
    216 
    217 #[derive(Clone, PartialEq, Eq)]
    218 pub struct RadrootsTradeEvidenceManifestV1 {
    219     trade_id: TradeId,
    220     trade_generation: NonZeroU64,
    221     evidence_policy_digest: RadrootsTradeEvidencePolicyDigestV1,
    222     observed_at_unix_s: u64,
    223     scope_prerequisites: RadrootsTradeEvidenceScopePrerequisitesV1,
    224     coverage: RadrootsTradeEvidenceCoverageV1,
    225     sources: Box<[RadrootsTradeEvidenceManifestSourceResultV1]>,
    226     observations: Box<[RadrootsTradeEvidenceManifestObservationV1]>,
    227     canonical_bytes: Box<[u8]>,
    228     digest: RadrootsTradeEvidenceManifestDigestV1,
    229 }
    230 
    231 impl RadrootsTradeEvidenceManifestV1 {
    232     pub fn new<S, O>(
    233         trade_id: TradeId,
    234         trade_generation: NonZeroU64,
    235         evidence_policy_digest: RadrootsTradeEvidencePolicyDigestV1,
    236         observed_at_unix_s: u64,
    237         scope_prerequisites: RadrootsTradeEvidenceScopePrerequisitesV1,
    238         sources: S,
    239         observations: O,
    240     ) -> Result<Self, RadrootsTradeEvidenceManifestError>
    241     where
    242         S: IntoIterator<Item = RadrootsTradeEvidenceManifestSourceResultV1>,
    243         O: IntoIterator<Item = RadrootsTradeEvidenceManifestObservationV1>,
    244     {
    245         let mut sources = collect_bounded(
    246             sources,
    247             RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT,
    248             RadrootsTradeEvidenceManifestError::SourceCountOutOfRange,
    249         )?;
    250         if sources.is_empty() {
    251             return Err(RadrootsTradeEvidenceManifestError::SourceCountOutOfRange);
    252         }
    253         sources.sort_by(|left, right| left.source_id.cmp(&right.source_id));
    254         if sources
    255             .windows(2)
    256             .any(|pair| pair[0].source_id == pair[1].source_id)
    257         {
    258             return Err(RadrootsTradeEvidenceManifestError::DuplicateSource);
    259         }
    260 
    261         let coverage = classify_trade_evidence_coverage_v1(
    262             sources.iter().map(|source| source.result),
    263             scope_prerequisites,
    264         )
    265         .map_err(map_coverage_error)?;
    266 
    267         let mut observations = collect_bounded(
    268             observations,
    269             RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS,
    270             RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange,
    271         )?;
    272         observations.sort_by(|left, right| {
    273             left.event_id
    274                 .as_bytes()
    275                 .cmp(right.event_id.as_bytes())
    276                 .then_with(|| left.source_id.cmp(&right.source_id))
    277                 .then_with(|| {
    278                     left.mutation_id
    279                         .as_bytes()
    280                         .cmp(right.mutation_id.as_bytes())
    281                 })
    282                 .then_with(|| left.signed_event_digest.cmp(&right.signed_event_digest))
    283                 .then_with(|| left.provenance_digest.cmp(&right.provenance_digest))
    284         });
    285         for pair in observations.windows(2) {
    286             if pair[0].event_id == pair[1].event_id
    287                 && (pair[0].mutation_id != pair[1].mutation_id
    288                     || pair[0].signed_event_digest != pair[1].signed_event_digest)
    289             {
    290                 return Err(RadrootsTradeEvidenceManifestError::ConflictingEvent);
    291             }
    292             if pair[0].event_id == pair[1].event_id && pair[0].source_id == pair[1].source_id {
    293                 return Err(RadrootsTradeEvidenceManifestError::DuplicateObservation);
    294             }
    295         }
    296 
    297         let mut actual_counts = vec![0_u32; sources.len()];
    298         for observation in &observations {
    299             let source_index = sources
    300                 .binary_search_by(|source| source.source_id.cmp(&observation.source_id))
    301                 .map_err(|_| RadrootsTradeEvidenceManifestError::UnknownObservationSource)?;
    302             actual_counts[source_index] = actual_counts[source_index]
    303                 .checked_add(1)
    304                 .ok_or(RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange)?;
    305         }
    306         if sources
    307             .iter()
    308             .zip(actual_counts)
    309             .any(|(source, actual)| source.result.admitted_event_count() != actual)
    310         {
    311             return Err(RadrootsTradeEvidenceManifestError::SourceEventCountMismatch);
    312         }
    313 
    314         let canonical_bytes = encode_manifest(
    315             &trade_id,
    316             trade_generation,
    317             evidence_policy_digest,
    318             observed_at_unix_s,
    319             scope_prerequisites,
    320             &sources,
    321             &observations,
    322         )?;
    323         let digest = manifest_digest(canonical_bytes.as_slice())?;
    324 
    325         Ok(Self {
    326             trade_id,
    327             trade_generation,
    328             evidence_policy_digest,
    329             observed_at_unix_s,
    330             scope_prerequisites,
    331             coverage,
    332             sources: sources.into_boxed_slice(),
    333             observations: observations.into_boxed_slice(),
    334             canonical_bytes: canonical_bytes.into_boxed_slice(),
    335             digest,
    336         })
    337     }
    338 
    339     pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, RadrootsTradeEvidenceManifestError> {
    340         if bytes.is_empty() {
    341             return Err(RadrootsTradeEvidenceManifestError::Malformed);
    342         }
    343         if bytes.len() > RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES {
    344             return Err(RadrootsTradeEvidenceManifestError::ManifestTooLarge);
    345         }
    346 
    347         let mut reader = ManifestReader::new(bytes);
    348         if reader.take(MANIFEST_PREFIX.len())? != MANIFEST_PREFIX {
    349             return Err(RadrootsTradeEvidenceManifestError::Malformed);
    350         }
    351         if reader.u16()? != RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION {
    352             return Err(RadrootsTradeEvidenceManifestError::UnsupportedVersion);
    353         }
    354 
    355         let trade_id = TradeId::from_bytes(reader.array()?);
    356         let trade_generation =
    357             NonZeroU64::new(reader.u64()?).ok_or(RadrootsTradeEvidenceManifestError::Malformed)?;
    358         let evidence_policy_digest =
    359             RadrootsTradeEvidencePolicyDigestV1::from_bytes(reader.array()?);
    360         let observed_at_unix_s = reader.u64()?;
    361         let scope_prerequisites = decode_scope_prerequisites(reader.u8()?)?;
    362 
    363         let source_count = usize::from(reader.u8()?);
    364         if source_count == 0 || source_count > RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT {
    365             return Err(RadrootsTradeEvidenceManifestError::SourceCountOutOfRange);
    366         }
    367         let mut sources = Vec::with_capacity(source_count);
    368         for _ in 0..source_count {
    369             let source_id = decode_source_id(&mut reader)?;
    370             let requirement = decode_requirement(reader.u8()?)?;
    371             let completion = decode_completion(reader.u8()?)?;
    372             let admitted_event_count = reader.u32()?;
    373             let result = RadrootsTradeEvidenceSourceResultV1::new(
    374                 requirement,
    375                 completion,
    376                 admitted_event_count,
    377             )
    378             .map_err(map_coverage_error)?;
    379             let result_digest =
    380                 RadrootsTradeEvidenceSourceResultDigestV1::from_bytes(reader.array()?);
    381             sources.push(RadrootsTradeEvidenceManifestSourceResultV1::new(
    382                 source_id,
    383                 result,
    384                 result_digest,
    385             ));
    386         }
    387 
    388         let observation_count = usize::try_from(reader.u32()?)
    389             .map_err(|_| RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange)?;
    390         if observation_count > RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS {
    391             return Err(RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange);
    392         }
    393         let mut observations = Vec::with_capacity(observation_count);
    394         for _ in 0..observation_count {
    395             let source_index = usize::from(reader.u8()?);
    396             let source = sources
    397                 .get(source_index)
    398                 .ok_or(RadrootsTradeEvidenceManifestError::UnknownObservationSource)?;
    399             observations.push(RadrootsTradeEvidenceManifestObservationV1::new(
    400                 source.source_id.clone(),
    401                 MutationId::from_bytes(reader.array()?),
    402                 EventId::from_bytes(reader.array()?),
    403                 RadrootsTradeSignedEventDigestV1::from_bytes(reader.array()?),
    404                 RadrootsTradeEvidenceProvenanceDigestV1::from_bytes(reader.array()?),
    405             ));
    406         }
    407         reader.finish()?;
    408 
    409         let manifest = Self::new(
    410             trade_id,
    411             trade_generation,
    412             evidence_policy_digest,
    413             observed_at_unix_s,
    414             scope_prerequisites,
    415             sources,
    416             observations,
    417         )?;
    418         if manifest.canonical_bytes.as_ref() != bytes {
    419             return Err(RadrootsTradeEvidenceManifestError::NonCanonical);
    420         }
    421         Ok(manifest)
    422     }
    423 
    424     pub const fn contract_id(&self) -> &'static str {
    425         RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID
    426     }
    427 
    428     pub const fn contract_version(&self) -> u16 {
    429         RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION
    430     }
    431 
    432     pub const fn trade_id(&self) -> &TradeId {
    433         &self.trade_id
    434     }
    435 
    436     pub const fn trade_generation(&self) -> NonZeroU64 {
    437         self.trade_generation
    438     }
    439 
    440     pub const fn evidence_policy_digest(&self) -> RadrootsTradeEvidencePolicyDigestV1 {
    441         self.evidence_policy_digest
    442     }
    443 
    444     pub const fn observed_at_unix_s(&self) -> u64 {
    445         self.observed_at_unix_s
    446     }
    447 
    448     pub const fn scope_prerequisites(&self) -> RadrootsTradeEvidenceScopePrerequisitesV1 {
    449         self.scope_prerequisites
    450     }
    451 
    452     pub const fn coverage(&self) -> RadrootsTradeEvidenceCoverageV1 {
    453         self.coverage
    454     }
    455 
    456     pub fn sources(&self) -> &[RadrootsTradeEvidenceManifestSourceResultV1] {
    457         &self.sources
    458     }
    459 
    460     pub fn observations(&self) -> &[RadrootsTradeEvidenceManifestObservationV1] {
    461         &self.observations
    462     }
    463 
    464     pub fn canonical_bytes(&self) -> &[u8] {
    465         &self.canonical_bytes
    466     }
    467 
    468     pub const fn digest(&self) -> RadrootsTradeEvidenceManifestDigestV1 {
    469         self.digest
    470     }
    471 }
    472 
    473 impl fmt::Debug for RadrootsTradeEvidenceManifestV1 {
    474     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    475         formatter
    476             .debug_struct("RadrootsTradeEvidenceManifestV1")
    477             .field("source_count", &self.sources.len())
    478             .field("observation_count", &self.observations.len())
    479             .finish_non_exhaustive()
    480     }
    481 }
    482 
    483 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    484 pub enum RadrootsTradeEvidenceManifestError {
    485     InvalidSourceId,
    486     SourceCountOutOfRange,
    487     DuplicateSource,
    488     NoRequiredSource,
    489     ObservationCountOutOfRange,
    490     UnknownObservationSource,
    491     DuplicateObservation,
    492     ConflictingEvent,
    493     SourceEventCountMismatch,
    494     ManifestTooLarge,
    495     Malformed,
    496     UnsupportedVersion,
    497     NonCanonical,
    498 }
    499 
    500 impl fmt::Display for RadrootsTradeEvidenceManifestError {
    501     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    502         formatter.write_str(match self {
    503             Self::InvalidSourceId => "evidence manifest source identity is invalid",
    504             Self::SourceCountOutOfRange => "evidence manifest source count is out of range",
    505             Self::DuplicateSource => "evidence manifest contains a duplicate source",
    506             Self::NoRequiredSource => "evidence manifest has no required source",
    507             Self::ObservationCountOutOfRange => {
    508                 "evidence manifest observation count is out of range"
    509             }
    510             Self::UnknownObservationSource => {
    511                 "evidence manifest observation references an unknown source"
    512             }
    513             Self::DuplicateObservation => "evidence manifest contains a duplicate observation",
    514             Self::ConflictingEvent => {
    515                 "evidence manifest contains conflicting records for one event"
    516             }
    517             Self::SourceEventCountMismatch => {
    518                 "evidence manifest source event count does not match its observations"
    519             }
    520             Self::ManifestTooLarge => "evidence manifest exceeds its canonical byte limit",
    521             Self::Malformed => "evidence manifest encoding is malformed",
    522             Self::UnsupportedVersion => "evidence manifest version is unsupported",
    523             Self::NonCanonical => "evidence manifest encoding is not canonical",
    524         })
    525     }
    526 }
    527 
    528 #[cfg(feature = "std")]
    529 impl std::error::Error for RadrootsTradeEvidenceManifestError {}
    530 
    531 fn collect_bounded<T>(
    532     values: impl IntoIterator<Item = T>,
    533     maximum: usize,
    534     error: RadrootsTradeEvidenceManifestError,
    535 ) -> Result<Vec<T>, RadrootsTradeEvidenceManifestError> {
    536     let mut collected = Vec::new();
    537     for value in values {
    538         if collected.len() == maximum {
    539             return Err(error);
    540         }
    541         collected.push(value);
    542     }
    543     Ok(collected)
    544 }
    545 
    546 fn map_coverage_error(
    547     error: RadrootsTradeEvidenceCoverageError,
    548 ) -> RadrootsTradeEvidenceManifestError {
    549     match error {
    550         RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange => {
    551             RadrootsTradeEvidenceManifestError::SourceCountOutOfRange
    552         }
    553         RadrootsTradeEvidenceCoverageError::NoRequiredSource => {
    554             RadrootsTradeEvidenceManifestError::NoRequiredSource
    555         }
    556         RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange => {
    557             RadrootsTradeEvidenceManifestError::SourceEventCountMismatch
    558         }
    559     }
    560 }
    561 
    562 fn encode_manifest(
    563     trade_id: &TradeId,
    564     trade_generation: NonZeroU64,
    565     evidence_policy_digest: RadrootsTradeEvidencePolicyDigestV1,
    566     observed_at_unix_s: u64,
    567     scope_prerequisites: RadrootsTradeEvidenceScopePrerequisitesV1,
    568     sources: &[RadrootsTradeEvidenceManifestSourceResultV1],
    569     observations: &[RadrootsTradeEvidenceManifestObservationV1],
    570 ) -> Result<Vec<u8>, RadrootsTradeEvidenceManifestError> {
    571     let mut bytes = Vec::new();
    572     bytes.extend_from_slice(MANIFEST_PREFIX);
    573     bytes.extend_from_slice(&RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION.to_be_bytes());
    574     bytes.extend_from_slice(trade_id.as_bytes());
    575     bytes.extend_from_slice(&trade_generation.get().to_be_bytes());
    576     bytes.extend_from_slice(evidence_policy_digest.as_bytes());
    577     bytes.extend_from_slice(&observed_at_unix_s.to_be_bytes());
    578     bytes.push(encode_scope_prerequisites(scope_prerequisites));
    579     bytes.push(
    580         u8::try_from(sources.len())
    581             .map_err(|_| RadrootsTradeEvidenceManifestError::SourceCountOutOfRange)?,
    582     );
    583     for source in sources {
    584         bytes.push(
    585             u8::try_from(source.source_id.as_str().len())
    586                 .map_err(|_| RadrootsTradeEvidenceManifestError::InvalidSourceId)?,
    587         );
    588         bytes.extend_from_slice(source.source_id.as_str().as_bytes());
    589         bytes.push(encode_requirement(source.result.requirement()));
    590         bytes.push(encode_completion(source.result.completion()));
    591         bytes.extend_from_slice(&source.result.admitted_event_count().to_be_bytes());
    592         bytes.extend_from_slice(source.result_digest.as_bytes());
    593     }
    594     bytes.extend_from_slice(
    595         &u32::try_from(observations.len())
    596             .map_err(|_| RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange)?
    597             .to_be_bytes(),
    598     );
    599     for observation in observations {
    600         let source_index = sources
    601             .binary_search_by(|source| source.source_id.cmp(&observation.source_id))
    602             .map_err(|_| RadrootsTradeEvidenceManifestError::UnknownObservationSource)?;
    603         bytes.push(
    604             u8::try_from(source_index)
    605                 .map_err(|_| RadrootsTradeEvidenceManifestError::SourceCountOutOfRange)?,
    606         );
    607         bytes.extend_from_slice(observation.mutation_id.as_bytes());
    608         bytes.extend_from_slice(observation.event_id.as_bytes());
    609         bytes.extend_from_slice(observation.signed_event_digest.as_bytes());
    610         bytes.extend_from_slice(observation.provenance_digest.as_bytes());
    611     }
    612     if bytes.len() > RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES {
    613         return Err(RadrootsTradeEvidenceManifestError::ManifestTooLarge);
    614     }
    615     Ok(bytes)
    616 }
    617 
    618 fn manifest_digest(
    619     bytes: &[u8],
    620 ) -> Result<RadrootsTradeEvidenceManifestDigestV1, RadrootsTradeEvidenceManifestError> {
    621     let length = u64::try_from(bytes.len())
    622         .map_err(|_| RadrootsTradeEvidenceManifestError::ManifestTooLarge)?;
    623     let mut hasher = Sha256::new();
    624     hasher.update(MANIFEST_DIGEST_DOMAIN);
    625     hasher.update(length.to_be_bytes());
    626     hasher.update(bytes);
    627     Ok(RadrootsTradeEvidenceManifestDigestV1(
    628         hasher.finalize().into(),
    629     ))
    630 }
    631 
    632 const fn encode_scope_prerequisites(value: RadrootsTradeEvidenceScopePrerequisitesV1) -> u8 {
    633     match value {
    634         RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied => 0,
    635         RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied => 1,
    636     }
    637 }
    638 
    639 fn decode_scope_prerequisites(
    640     value: u8,
    641 ) -> Result<RadrootsTradeEvidenceScopePrerequisitesV1, RadrootsTradeEvidenceManifestError> {
    642     match value {
    643         0 => Ok(RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied),
    644         1 => Ok(RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied),
    645         _ => Err(RadrootsTradeEvidenceManifestError::Malformed),
    646     }
    647 }
    648 
    649 const fn encode_requirement(value: RadrootsTradeEvidenceSourceRequirementV1) -> u8 {
    650     match value {
    651         RadrootsTradeEvidenceSourceRequirementV1::Required => 0,
    652         RadrootsTradeEvidenceSourceRequirementV1::Optional => 1,
    653     }
    654 }
    655 
    656 fn decode_requirement(
    657     value: u8,
    658 ) -> Result<RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceManifestError> {
    659     match value {
    660         0 => Ok(RadrootsTradeEvidenceSourceRequirementV1::Required),
    661         1 => Ok(RadrootsTradeEvidenceSourceRequirementV1::Optional),
    662         _ => Err(RadrootsTradeEvidenceManifestError::Malformed),
    663     }
    664 }
    665 
    666 const fn encode_completion(value: RadrootsTradeEvidenceSourceCompletionV1) -> u8 {
    667     match value {
    668         RadrootsTradeEvidenceSourceCompletionV1::Complete => 0,
    669         RadrootsTradeEvidenceSourceCompletionV1::Incomplete => 1,
    670         RadrootsTradeEvidenceSourceCompletionV1::Unsupported => 2,
    671     }
    672 }
    673 
    674 fn decode_completion(
    675     value: u8,
    676 ) -> Result<RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceManifestError> {
    677     match value {
    678         0 => Ok(RadrootsTradeEvidenceSourceCompletionV1::Complete),
    679         1 => Ok(RadrootsTradeEvidenceSourceCompletionV1::Incomplete),
    680         2 => Ok(RadrootsTradeEvidenceSourceCompletionV1::Unsupported),
    681         _ => Err(RadrootsTradeEvidenceManifestError::Malformed),
    682     }
    683 }
    684 
    685 fn decode_source_id(
    686     reader: &mut ManifestReader<'_>,
    687 ) -> Result<RadrootsTradeEvidenceSourceIdV1, RadrootsTradeEvidenceManifestError> {
    688     let length = usize::from(reader.u8()?);
    689     if length == 0 || length > RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES {
    690         return Err(RadrootsTradeEvidenceManifestError::InvalidSourceId);
    691     }
    692     let value = core::str::from_utf8(reader.take(length)?)
    693         .map_err(|_| RadrootsTradeEvidenceManifestError::InvalidSourceId)?;
    694     RadrootsTradeEvidenceSourceIdV1::parse(value)
    695 }
    696 
    697 struct ManifestReader<'a> {
    698     bytes: &'a [u8],
    699     offset: usize,
    700 }
    701 
    702 impl<'a> ManifestReader<'a> {
    703     const fn new(bytes: &'a [u8]) -> Self {
    704         Self { bytes, offset: 0 }
    705     }
    706 
    707     fn take(&mut self, length: usize) -> Result<&'a [u8], RadrootsTradeEvidenceManifestError> {
    708         let end = self
    709             .offset
    710             .checked_add(length)
    711             .ok_or(RadrootsTradeEvidenceManifestError::Malformed)?;
    712         let value = self
    713             .bytes
    714             .get(self.offset..end)
    715             .ok_or(RadrootsTradeEvidenceManifestError::Malformed)?;
    716         self.offset = end;
    717         Ok(value)
    718     }
    719 
    720     fn array<const N: usize>(&mut self) -> Result<[u8; N], RadrootsTradeEvidenceManifestError> {
    721         self.take(N)?
    722             .try_into()
    723             .map_err(|_| RadrootsTradeEvidenceManifestError::Malformed)
    724     }
    725 
    726     fn u8(&mut self) -> Result<u8, RadrootsTradeEvidenceManifestError> {
    727         Ok(self.array::<1>()?[0])
    728     }
    729 
    730     fn u16(&mut self) -> Result<u16, RadrootsTradeEvidenceManifestError> {
    731         Ok(u16::from_be_bytes(self.array()?))
    732     }
    733 
    734     fn u32(&mut self) -> Result<u32, RadrootsTradeEvidenceManifestError> {
    735         Ok(u32::from_be_bytes(self.array()?))
    736     }
    737 
    738     fn u64(&mut self) -> Result<u64, RadrootsTradeEvidenceManifestError> {
    739         Ok(u64::from_be_bytes(self.array()?))
    740     }
    741 
    742     fn finish(self) -> Result<(), RadrootsTradeEvidenceManifestError> {
    743         if self.offset == self.bytes.len() {
    744             Ok(())
    745         } else {
    746             Err(RadrootsTradeEvidenceManifestError::NonCanonical)
    747         }
    748     }
    749 }
    750 
    751 #[cfg(test)]
    752 mod tests {
    753     use alloc::{format, string::ToString, vec};
    754 
    755     use super::*;
    756 
    757     fn source_id(value: &str) -> RadrootsTradeEvidenceSourceIdV1 {
    758         RadrootsTradeEvidenceSourceIdV1::parse(value).unwrap()
    759     }
    760 
    761     fn policy_digest(byte: u8) -> RadrootsTradeEvidencePolicyDigestV1 {
    762         RadrootsTradeEvidencePolicyDigestV1::from_bytes([byte; 32])
    763     }
    764 
    765     fn source_result_digest(byte: u8) -> RadrootsTradeEvidenceSourceResultDigestV1 {
    766         RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([byte; 32])
    767     }
    768 
    769     fn signed_event_digest(byte: u8) -> RadrootsTradeSignedEventDigestV1 {
    770         RadrootsTradeSignedEventDigestV1::from_bytes([byte; 32])
    771     }
    772 
    773     fn provenance_digest(byte: u8) -> RadrootsTradeEvidenceProvenanceDigestV1 {
    774         RadrootsTradeEvidenceProvenanceDigestV1::from_bytes([byte; 32])
    775     }
    776 
    777     fn event_id(byte: u8) -> EventId {
    778         EventId::from_bytes([byte; 32])
    779     }
    780 
    781     fn mutation_id(byte: u8) -> MutationId {
    782         MutationId::from_bytes([byte; 32])
    783     }
    784 
    785     fn ordinal_ids(ordinal: u32) -> (MutationId, EventId) {
    786         let mut mutation = [0_u8; 32];
    787         mutation[28..].copy_from_slice(&ordinal.to_be_bytes());
    788         let mut event = mutation;
    789         event[0] = 1;
    790         (MutationId::from_bytes(mutation), EventId::from_bytes(event))
    791     }
    792 
    793     fn source(
    794         id: &str,
    795         requirement: RadrootsTradeEvidenceSourceRequirementV1,
    796         completion: RadrootsTradeEvidenceSourceCompletionV1,
    797         count: u32,
    798         digest_byte: u8,
    799     ) -> RadrootsTradeEvidenceManifestSourceResultV1 {
    800         RadrootsTradeEvidenceManifestSourceResultV1::new(
    801             source_id(id),
    802             RadrootsTradeEvidenceSourceResultV1::new(requirement, completion, count).unwrap(),
    803             source_result_digest(digest_byte),
    804         )
    805     }
    806 
    807     fn observation(
    808         source: &str,
    809         mutation: u8,
    810         event: u8,
    811         signed: u8,
    812         provenance: u8,
    813     ) -> RadrootsTradeEvidenceManifestObservationV1 {
    814         RadrootsTradeEvidenceManifestObservationV1::new(
    815             source_id(source),
    816             mutation_id(mutation),
    817             event_id(event),
    818             signed_event_digest(signed),
    819             provenance_digest(provenance),
    820         )
    821     }
    822 
    823     fn manifest() -> RadrootsTradeEvidenceManifestV1 {
    824         RadrootsTradeEvidenceManifestV1::new(
    825             TradeId::from_bytes([0x11; 16]),
    826             NonZeroU64::new(7).unwrap(),
    827             policy_digest(0x22),
    828             1_700_000_000,
    829             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    830             [
    831                 source(
    832                     "source-b",
    833                     RadrootsTradeEvidenceSourceRequirementV1::Optional,
    834                     RadrootsTradeEvidenceSourceCompletionV1::Incomplete,
    835                     1,
    836                     0x42,
    837                 ),
    838                 source(
    839                     "source-a",
    840                     RadrootsTradeEvidenceSourceRequirementV1::Required,
    841                     RadrootsTradeEvidenceSourceCompletionV1::Complete,
    842                     1,
    843                     0x41,
    844                 ),
    845             ],
    846             [
    847                 observation("source-b", 0x32, 0x52, 0x62, 0x72),
    848                 observation("source-a", 0x31, 0x51, 0x61, 0x71),
    849             ],
    850         )
    851         .unwrap()
    852     }
    853 
    854     #[test]
    855     fn exact_manifest_vector_and_digest_are_frozen() {
    856         let manifest = manifest();
    857         assert_eq!(
    858             hex::encode(manifest.canonical_bytes()),
    859             "726164726f6f74732e74726164652e65766964656e63652d6d616e69666573742e76310000011111111111111111111111111111111100000000000000072222222222222222222222222222222222222222222222222222222222222222000000006553f100000208736f757263652d61000000000001414141414141414141414141414141414141414141414141414141414141414108736f757263652d62010100000001424242424242424242424242424242424242424242424242424242424242424200000002003131313131313131313131313131313131313131313131313131313131313131515151515151515151515151515151515151515151515151515151515151515161616161616161616161616161616161616161616161616161616161616161617171717171717171717171717171717171717171717171717171717171717171013232323232323232323232323232323232323232323232323232323232323232525252525252525252525252525252525252525252525252525252525252525262626262626262626262626262626262626262626262626262626262626262627272727272727272727272727272727272727272727272727272727272727272"
    860         );
    861         assert_eq!(
    862             manifest.digest().to_hex(),
    863             "ec5e2cdc85107afe43c90925b695fe9cd8605970886f1d2a2d431b602adeb3a9"
    864         );
    865         assert_eq!(
    866             manifest.contract_id(),
    867             RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID
    868         );
    869         assert_eq!(manifest.contract_version(), 1);
    870         assert_eq!(
    871             manifest.coverage(),
    872             RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
    873         );
    874     }
    875 
    876     #[test]
    877     fn manifest_components_expose_exact_typed_evidence() {
    878         let manifest = manifest();
    879         let source = &manifest.sources()[0];
    880         let observation = &manifest.observations()[0];
    881 
    882         assert_eq!(source.source_id().as_str(), "source-a");
    883         assert_eq!(
    884             source.result().requirement(),
    885             RadrootsTradeEvidenceSourceRequirementV1::Required
    886         );
    887         assert_eq!(
    888             source.result().completion(),
    889             RadrootsTradeEvidenceSourceCompletionV1::Complete
    890         );
    891         assert_eq!(source.result().admitted_event_count(), 1);
    892         assert_eq!(source.result_digest().as_bytes(), &[0x41; 32]);
    893 
    894         assert_eq!(observation.source_id().as_str(), "source-a");
    895         assert_eq!(observation.mutation_id().as_bytes(), &[0x31; 32]);
    896         assert_eq!(observation.event_id().as_bytes(), &[0x51; 32]);
    897         assert_eq!(observation.signed_event_digest().as_bytes(), &[0x61; 32]);
    898         assert_eq!(observation.provenance_digest().as_bytes(), &[0x71; 32]);
    899         assert_eq!(manifest.digest().as_bytes().len(), 32);
    900 
    901         assert_eq!(
    902             RadrootsTradeEvidencePolicyDigestV1::sha256(b"policy").as_bytes(),
    903             RadrootsTradeEvidencePolicyDigestV1::from_bytes(Sha256::digest(b"policy").into())
    904                 .as_bytes()
    905         );
    906         assert_eq!(
    907             RadrootsTradeEvidenceSourceResultDigestV1::sha256(b"result").as_bytes(),
    908             RadrootsTradeEvidenceSourceResultDigestV1::from_bytes(Sha256::digest(b"result").into())
    909                 .as_bytes()
    910         );
    911         assert_eq!(
    912             RadrootsTradeSignedEventDigestV1::sha256(b"event").as_bytes(),
    913             RadrootsTradeSignedEventDigestV1::from_bytes(Sha256::digest(b"event").into())
    914                 .as_bytes()
    915         );
    916         assert_eq!(
    917             RadrootsTradeEvidenceProvenanceDigestV1::sha256(b"provenance").as_bytes(),
    918             RadrootsTradeEvidenceProvenanceDigestV1::from_bytes(
    919                 Sha256::digest(b"provenance").into()
    920             )
    921             .as_bytes()
    922         );
    923     }
    924 
    925     #[test]
    926     fn construction_is_permutation_invariant_and_parser_is_strict() {
    927         let first = manifest();
    928         let second = RadrootsTradeEvidenceManifestV1::new(
    929             *first.trade_id(),
    930             first.trade_generation(),
    931             first.evidence_policy_digest(),
    932             first.observed_at_unix_s(),
    933             first.scope_prerequisites(),
    934             first.sources().iter().cloned().rev(),
    935             first.observations().iter().cloned().rev(),
    936         )
    937         .unwrap();
    938         assert_eq!(first, second);
    939         assert_eq!(
    940             RadrootsTradeEvidenceManifestV1::from_canonical_bytes(first.canonical_bytes()),
    941             Ok(first.clone())
    942         );
    943 
    944         let mut trailing = first.canonical_bytes().to_vec();
    945         trailing.push(0);
    946         assert_eq!(
    947             RadrootsTradeEvidenceManifestV1::from_canonical_bytes(&trailing),
    948             Err(RadrootsTradeEvidenceManifestError::NonCanonical)
    949         );
    950         let mut version = first.canonical_bytes().to_vec();
    951         version[MANIFEST_PREFIX.len() + 1] = 2;
    952         assert_eq!(
    953             RadrootsTradeEvidenceManifestV1::from_canonical_bytes(&version),
    954             Err(RadrootsTradeEvidenceManifestError::UnsupportedVersion)
    955         );
    956         for length in 0..first.canonical_bytes().len() {
    957             assert!(
    958                 RadrootsTradeEvidenceManifestV1::from_canonical_bytes(
    959                     &first.canonical_bytes()[..length]
    960                 )
    961                 .is_err()
    962             );
    963         }
    964     }
    965 
    966     #[test]
    967     fn manifest_rejects_unknown_duplicate_and_mismatched_inventory() {
    968         let required = source(
    969             "required",
    970             RadrootsTradeEvidenceSourceRequirementV1::Required,
    971             RadrootsTradeEvidenceSourceCompletionV1::Complete,
    972             1,
    973             1,
    974         );
    975         let base = |sources: Vec<_>, observations: Vec<_>| {
    976             RadrootsTradeEvidenceManifestV1::new(
    977                 TradeId::from_bytes([1; 16]),
    978                 NonZeroU64::new(1).unwrap(),
    979                 policy_digest(2),
    980                 3,
    981                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    982                 sources,
    983                 observations,
    984             )
    985         };
    986         assert_eq!(
    987             base(
    988                 vec![required.clone()],
    989                 vec![observation("other", 1, 2, 3, 4)]
    990             ),
    991             Err(RadrootsTradeEvidenceManifestError::UnknownObservationSource)
    992         );
    993         assert_eq!(
    994             base(vec![required.clone(), required.clone()], vec![]),
    995             Err(RadrootsTradeEvidenceManifestError::DuplicateSource)
    996         );
    997         let observed = observation("required", 1, 2, 3, 4);
    998         assert_eq!(
    999             base(vec![required.clone()], vec![observed.clone(), observed]),
   1000             Err(RadrootsTradeEvidenceManifestError::DuplicateObservation)
   1001         );
   1002         let conflicting = RadrootsTradeEvidenceManifestObservationV1::new(
   1003             source_id("required"),
   1004             mutation_id(1),
   1005             event_id(2),
   1006             signed_event_digest(9),
   1007             provenance_digest(10),
   1008         );
   1009         assert_eq!(
   1010             base(
   1011                 vec![required.clone()],
   1012                 vec![observation("required", 1, 2, 3, 4), conflicting]
   1013             ),
   1014             Err(RadrootsTradeEvidenceManifestError::ConflictingEvent)
   1015         );
   1016         let optional = source(
   1017             "optional",
   1018             RadrootsTradeEvidenceSourceRequirementV1::Optional,
   1019             RadrootsTradeEvidenceSourceCompletionV1::Complete,
   1020             1,
   1021             2,
   1022         );
   1023         assert_eq!(
   1024             base(
   1025                 vec![required.clone(), optional],
   1026                 vec![
   1027                     observation("required", 1, 2, 3, 4),
   1028                     observation("optional", 9, 2, 3, 5),
   1029                 ]
   1030             ),
   1031             Err(RadrootsTradeEvidenceManifestError::ConflictingEvent)
   1032         );
   1033         assert_eq!(
   1034             base(vec![required], vec![]),
   1035             Err(RadrootsTradeEvidenceManifestError::SourceEventCountMismatch)
   1036         );
   1037     }
   1038 
   1039     #[test]
   1040     fn source_identity_and_inventory_bounds_are_exact() {
   1041         for invalid in ["", "Upper", "-prefix", "has space", "a/b"] {
   1042             assert_eq!(
   1043                 RadrootsTradeEvidenceSourceIdV1::parse(invalid),
   1044                 Err(RadrootsTradeEvidenceManifestError::InvalidSourceId)
   1045             );
   1046         }
   1047         let maximum = format!("a{}", "1".repeat(63));
   1048         assert_eq!(source_id(&maximum).as_str(), maximum);
   1049         assert_eq!(
   1050             RadrootsTradeEvidenceSourceIdV1::parse(format!("a{}", "1".repeat(64))),
   1051             Err(RadrootsTradeEvidenceManifestError::InvalidSourceId)
   1052         );
   1053 
   1054         let required = source(
   1055             "required",
   1056             RadrootsTradeEvidenceSourceRequirementV1::Required,
   1057             RadrootsTradeEvidenceSourceCompletionV1::Incomplete,
   1058             0,
   1059             1,
   1060         );
   1061         fn create(
   1062             sources: impl IntoIterator<Item = RadrootsTradeEvidenceManifestSourceResultV1>,
   1063         ) -> Result<RadrootsTradeEvidenceManifestV1, RadrootsTradeEvidenceManifestError> {
   1064             RadrootsTradeEvidenceManifestV1::new(
   1065                 TradeId::from_bytes([1; 16]),
   1066                 NonZeroU64::new(1).unwrap(),
   1067                 policy_digest(2),
   1068                 3,
   1069                 RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied,
   1070                 sources,
   1071                 core::iter::empty(),
   1072             )
   1073         }
   1074         assert_eq!(
   1075             create(core::iter::empty()),
   1076             Err(RadrootsTradeEvidenceManifestError::SourceCountOutOfRange)
   1077         );
   1078         assert_eq!(
   1079             create(core::iter::repeat(required)),
   1080             Err(RadrootsTradeEvidenceManifestError::SourceCountOutOfRange)
   1081         );
   1082         assert_eq!(
   1083             create([source(
   1084                 "optional",
   1085                 RadrootsTradeEvidenceSourceRequirementV1::Optional,
   1086                 RadrootsTradeEvidenceSourceCompletionV1::Incomplete,
   1087                 0,
   1088                 1,
   1089             )]),
   1090             Err(RadrootsTradeEvidenceManifestError::NoRequiredSource)
   1091         );
   1092     }
   1093 
   1094     #[test]
   1095     fn observation_and_canonical_input_bounds_are_exact() {
   1096         let sources = (0..RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT).map(|index| {
   1097             source(
   1098                 &format!("source-{index:02}"),
   1099                 if index == 0 {
   1100                     RadrootsTradeEvidenceSourceRequirementV1::Required
   1101                 } else {
   1102                     RadrootsTradeEvidenceSourceRequirementV1::Optional
   1103                 },
   1104                 RadrootsTradeEvidenceSourceCompletionV1::Complete,
   1105                 RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
   1106                 u8::try_from(index).unwrap(),
   1107             )
   1108         });
   1109         let observations =
   1110             (0..RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS).map(|index| {
   1111                 let source_index = index
   1112                     / usize::try_from(RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE).unwrap();
   1113                 let (mutation_id, event_id) = ordinal_ids(u32::try_from(index).unwrap());
   1114                 RadrootsTradeEvidenceManifestObservationV1::new(
   1115                     source_id(&format!("source-{source_index:02}")),
   1116                     mutation_id,
   1117                     event_id,
   1118                     signed_event_digest(1),
   1119                     provenance_digest(2),
   1120                 )
   1121             });
   1122         let maximum = RadrootsTradeEvidenceManifestV1::new(
   1123             TradeId::from_bytes([1; 16]),
   1124             NonZeroU64::new(1).unwrap(),
   1125             policy_digest(2),
   1126             3,
   1127             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
   1128             sources,
   1129             observations,
   1130         )
   1131         .unwrap();
   1132         assert_eq!(
   1133             maximum.observations().len(),
   1134             RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS
   1135         );
   1136         assert_eq!(
   1137             RadrootsTradeEvidenceManifestV1::from_canonical_bytes(maximum.canonical_bytes()),
   1138             Ok(maximum)
   1139         );
   1140 
   1141         let required = source(
   1142             "required",
   1143             RadrootsTradeEvidenceSourceRequirementV1::Required,
   1144             RadrootsTradeEvidenceSourceCompletionV1::Incomplete,
   1145             0,
   1146             1,
   1147         );
   1148         assert_eq!(
   1149             RadrootsTradeEvidenceManifestV1::new(
   1150                 TradeId::from_bytes([1; 16]),
   1151                 NonZeroU64::new(1).unwrap(),
   1152                 policy_digest(2),
   1153                 3,
   1154                 RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied,
   1155                 [required],
   1156                 core::iter::repeat(observation("required", 1, 2, 3, 4)),
   1157             ),
   1158             Err(RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange)
   1159         );
   1160         assert_eq!(
   1161             RadrootsTradeEvidenceManifestV1::from_canonical_bytes(&vec![
   1162                 0;
   1163                 RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES
   1164                     + 1
   1165             ]),
   1166             Err(RadrootsTradeEvidenceManifestError::ManifestTooLarge)
   1167         );
   1168     }
   1169 
   1170     #[test]
   1171     fn debug_and_errors_do_not_expose_evidence_identity() {
   1172         let manifest = manifest();
   1173         let debug = format!("{manifest:?}");
   1174         for secret in ["source-a", "source-b", &manifest.trade_id().to_string()] {
   1175             assert!(!debug.contains(secret));
   1176         }
   1177         assert_eq!(
   1178             format!("{:?}", manifest.sources()[0]),
   1179             "RadrootsTradeEvidenceManifestSourceResultV1(<redacted>)"
   1180         );
   1181         assert_eq!(
   1182             format!("{:?}", manifest.observations()[0]),
   1183             "RadrootsTradeEvidenceManifestObservationV1(<redacted>)"
   1184         );
   1185         for error in [
   1186             RadrootsTradeEvidenceManifestError::InvalidSourceId,
   1187             RadrootsTradeEvidenceManifestError::SourceCountOutOfRange,
   1188             RadrootsTradeEvidenceManifestError::DuplicateSource,
   1189             RadrootsTradeEvidenceManifestError::NoRequiredSource,
   1190             RadrootsTradeEvidenceManifestError::ObservationCountOutOfRange,
   1191             RadrootsTradeEvidenceManifestError::UnknownObservationSource,
   1192             RadrootsTradeEvidenceManifestError::DuplicateObservation,
   1193             RadrootsTradeEvidenceManifestError::ConflictingEvent,
   1194             RadrootsTradeEvidenceManifestError::SourceEventCountMismatch,
   1195             RadrootsTradeEvidenceManifestError::ManifestTooLarge,
   1196             RadrootsTradeEvidenceManifestError::Malformed,
   1197             RadrootsTradeEvidenceManifestError::UnsupportedVersion,
   1198             RadrootsTradeEvidenceManifestError::NonCanonical,
   1199         ] {
   1200             assert!(!error.to_string().is_empty());
   1201             #[cfg(feature = "std")]
   1202             assert!(std::error::Error::source(&error).is_none());
   1203         }
   1204     }
   1205 }