lib

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

evidence.rs (16277B)


      1 //! Evidence supplied explicitly to deterministic trade reduction.
      2 //!
      3 //! These values describe observed mutation, private-term, and attestation
      4 //! records. They perform no retrieval, signature verification, or decryption.
      5 
      6 use core::fmt;
      7 
      8 /// Maximum number of evidence sources admitted by the v1 coverage evaluator.
      9 pub const RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT: usize = 16;
     10 
     11 /// Maximum admitted event count represented for one v1 evidence source.
     12 pub const RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE: u32 = 4_096;
     13 
     14 /// Evidence coverage established for one governed reconciliation scope.
     15 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     16 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     17 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
     18 pub enum RadrootsTradeEvidenceCoverageV1 {
     19     /// No source completed and no relevant evidence was admitted.
     20     #[default]
     21     Missing,
     22     /// Some relevant completion or evidence exists, but required scope is not satisfied.
     23     Partial,
     24     /// Every required source completed and every required scope prerequisite is satisfied.
     25     ScopeSatisfied,
     26     /// At least one required source cannot evaluate the governed scope.
     27     Unsupported,
     28 }
     29 
     30 impl RadrootsTradeEvidenceCoverageV1 {
     31     /// Returns whether this coverage permits the requested outcome.
     32     pub const fn permits(self, outcome: RadrootsTradeEvidenceOutcomeV1) -> bool {
     33         matches!(outcome, RadrootsTradeEvidenceOutcomeV1::Indeterminate)
     34             || matches!(self, Self::ScopeSatisfied)
     35     }
     36 }
     37 
     38 /// Result of evaluating a claim against one immutable evidence scope.
     39 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     40 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     41 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
     42 pub enum RadrootsTradeEvidenceOutcomeV1 {
     43     Valid,
     44     Invalid,
     45     #[default]
     46     Indeterminate,
     47 }
     48 
     49 /// Completion evidence retained for one configured source.
     50 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
     51 pub enum RadrootsTradeEvidenceSourceCompletionV1 {
     52     Complete,
     53     #[default]
     54     Incomplete,
     55     Unsupported,
     56 }
     57 
     58 /// Whether one configured evidence source is required by the governed scope.
     59 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
     60 pub enum RadrootsTradeEvidenceSourceRequirementV1 {
     61     Required,
     62     Optional,
     63 }
     64 
     65 /// Whether every non-source prerequisite for the governed scope is satisfied.
     66 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
     67 pub enum RadrootsTradeEvidenceScopePrerequisitesV1 {
     68     Satisfied,
     69     Unsatisfied,
     70 }
     71 
     72 /// Bounded source facts consumed by the v1 coverage evaluator.
     73 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     74 pub struct RadrootsTradeEvidenceSourceResultV1 {
     75     requirement: RadrootsTradeEvidenceSourceRequirementV1,
     76     completion: RadrootsTradeEvidenceSourceCompletionV1,
     77     admitted_event_count: u32,
     78 }
     79 
     80 impl RadrootsTradeEvidenceSourceResultV1 {
     81     pub const fn new(
     82         requirement: RadrootsTradeEvidenceSourceRequirementV1,
     83         completion: RadrootsTradeEvidenceSourceCompletionV1,
     84         admitted_event_count: u32,
     85     ) -> Result<Self, RadrootsTradeEvidenceCoverageError> {
     86         if admitted_event_count > RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE {
     87             return Err(RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange);
     88         }
     89         Ok(Self {
     90             requirement,
     91             completion,
     92             admitted_event_count,
     93         })
     94     }
     95 
     96     pub const fn requirement(&self) -> RadrootsTradeEvidenceSourceRequirementV1 {
     97         self.requirement
     98     }
     99 
    100     pub const fn completion(&self) -> RadrootsTradeEvidenceSourceCompletionV1 {
    101         self.completion
    102     }
    103 
    104     pub const fn admitted_event_count(&self) -> u32 {
    105         self.admitted_event_count
    106     }
    107 }
    108 
    109 /// Stable failures produced before coverage can be classified.
    110 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    111 pub enum RadrootsTradeEvidenceCoverageError {
    112     SourceCountOutOfRange,
    113     NoRequiredSource,
    114     AdmittedEventCountOutOfRange,
    115 }
    116 
    117 impl fmt::Display for RadrootsTradeEvidenceCoverageError {
    118     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    119         formatter.write_str(match self {
    120             Self::SourceCountOutOfRange => "evidence source count is out of range",
    121             Self::NoRequiredSource => "evidence scope has no required source",
    122             Self::AdmittedEventCountOutOfRange => {
    123                 "evidence source admitted event count is out of range"
    124             }
    125         })
    126     }
    127 }
    128 
    129 #[cfg(feature = "std")]
    130 impl std::error::Error for RadrootsTradeEvidenceCoverageError {}
    131 
    132 /// Classifies bounded source results using the governed v1 coverage precedence.
    133 ///
    134 /// The iterator is consumed only through the maximum source count plus one, so
    135 /// an excessive or infinite input terminates with `SourceCountOutOfRange`.
    136 pub fn classify_trade_evidence_coverage_v1(
    137     sources: impl IntoIterator<Item = RadrootsTradeEvidenceSourceResultV1>,
    138     scope_prerequisites: RadrootsTradeEvidenceScopePrerequisitesV1,
    139 ) -> Result<RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceCoverageError> {
    140     let mut source_count = 0_usize;
    141     let mut has_required_source = false;
    142     let mut required_source_unsupported = false;
    143     let mut every_required_source_complete = true;
    144     let mut any_source_complete = false;
    145     let mut any_relevant_evidence = false;
    146 
    147     for source in sources {
    148         source_count += 1;
    149         if source_count > RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT {
    150             return Err(RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange);
    151         }
    152 
    153         if matches!(
    154             source.requirement,
    155             RadrootsTradeEvidenceSourceRequirementV1::Required
    156         ) {
    157             has_required_source = true;
    158             required_source_unsupported |= matches!(
    159                 source.completion,
    160                 RadrootsTradeEvidenceSourceCompletionV1::Unsupported
    161             );
    162             every_required_source_complete &= matches!(
    163                 source.completion,
    164                 RadrootsTradeEvidenceSourceCompletionV1::Complete
    165             );
    166         }
    167         any_source_complete |= matches!(
    168             source.completion,
    169             RadrootsTradeEvidenceSourceCompletionV1::Complete
    170         );
    171         any_relevant_evidence |= source.admitted_event_count > 0;
    172     }
    173 
    174     if source_count == 0 {
    175         return Err(RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange);
    176     }
    177     if !has_required_source {
    178         return Err(RadrootsTradeEvidenceCoverageError::NoRequiredSource);
    179     }
    180 
    181     Ok(if required_source_unsupported {
    182         RadrootsTradeEvidenceCoverageV1::Unsupported
    183     } else if every_required_source_complete
    184         && matches!(
    185             scope_prerequisites,
    186             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
    187         )
    188     {
    189         RadrootsTradeEvidenceCoverageV1::ScopeSatisfied
    190     } else if any_source_complete || any_relevant_evidence {
    191         RadrootsTradeEvidenceCoverageV1::Partial
    192     } else {
    193         RadrootsTradeEvidenceCoverageV1::Missing
    194     })
    195 }
    196 
    197 pub use crate::trade_contract_v1::{
    198     RadrootsTradeAttestationRecordV1, RadrootsTradeAttestationResultV1,
    199     RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
    200     RadrootsTradePrivateTermsEvidenceV1,
    201 };
    202 
    203 #[cfg(feature = "json")]
    204 pub use crate::evidence_manifest::{
    205     RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_ID,
    206     RADROOTS_TRADE_EVIDENCE_MANIFEST_CONTRACT_VERSION,
    207     RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES,
    208     RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_OBSERVATIONS,
    209     RADROOTS_TRADE_EVIDENCE_SOURCE_ID_MAXIMUM_BYTES, RadrootsTradeEvidenceManifestDigestV1,
    210     RadrootsTradeEvidenceManifestError, RadrootsTradeEvidenceManifestObservationV1,
    211     RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1,
    212     RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceProvenanceDigestV1,
    213     RadrootsTradeEvidenceSourceIdV1, RadrootsTradeEvidenceSourceResultDigestV1,
    214     RadrootsTradeSignedEventDigestV1,
    215 };
    216 
    217 #[cfg(feature = "json")]
    218 pub use crate::evidence_report::{
    219     RADROOTS_RHI_EVIDENCE_ATTESTATION_METHOD, RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_ID,
    220     RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_VERSION,
    221     RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_CANONICAL_BYTES,
    222     RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_REASON_CODES,
    223     RADROOTS_RHI_EVIDENCE_REPORT_REASON_CODE_MAXIMUM_BYTES, RadrootsRhiEvidenceReasonCodeV1,
    224     RadrootsRhiEvidenceReportError, RadrootsRhiEvidenceReportV1,
    225     RadrootsRhiEvidenceStatementDigestV1, RadrootsRhiEvidenceSupersessionV1,
    226     RadrootsTradeEvidenceProjectionDigestV1,
    227 };
    228 
    229 #[cfg(test)]
    230 mod tests {
    231     #[cfg(not(feature = "std"))]
    232     use alloc::{format, string::ToString, vec};
    233 
    234     use super::*;
    235 
    236     fn source(
    237         required: bool,
    238         completion: RadrootsTradeEvidenceSourceCompletionV1,
    239         admitted_event_count: u32,
    240     ) -> RadrootsTradeEvidenceSourceResultV1 {
    241         let requirement = if required {
    242             RadrootsTradeEvidenceSourceRequirementV1::Required
    243         } else {
    244             RadrootsTradeEvidenceSourceRequirementV1::Optional
    245         };
    246         RadrootsTradeEvidenceSourceResultV1::new(requirement, completion, admitted_event_count)
    247             .expect("valid source result")
    248     }
    249 
    250     #[test]
    251     fn coverage_precedence_matches_the_governed_vectors() {
    252         use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
    253         use RadrootsTradeEvidenceSourceCompletionV1::{
    254             Complete, Incomplete, Unsupported as SourceUnsupported,
    255         };
    256 
    257         let vectors = [
    258             (vec![source(true, SourceUnsupported, 0)], false, Unsupported),
    259             (vec![source(true, Incomplete, 0)], false, Missing),
    260             (
    261                 vec![source(true, Incomplete, 0), source(false, Complete, 1)],
    262                 false,
    263                 Partial,
    264             ),
    265             (vec![source(true, Complete, 1)], false, Partial),
    266             (
    267                 vec![source(true, Complete, 1), source(false, Incomplete, 0)],
    268                 true,
    269                 ScopeSatisfied,
    270             ),
    271         ];
    272 
    273         for (sources, prerequisites, expected) in vectors {
    274             let prerequisites = if prerequisites {
    275                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied
    276             } else {
    277                 RadrootsTradeEvidenceScopePrerequisitesV1::Unsatisfied
    278             };
    279             assert_eq!(
    280                 classify_trade_evidence_coverage_v1(sources, prerequisites),
    281                 Ok(expected)
    282             );
    283         }
    284     }
    285 
    286     #[test]
    287     fn optional_sources_cannot_substitute_for_required_completion() {
    288         use RadrootsTradeEvidenceCoverageV1::{Missing, Partial};
    289         use RadrootsTradeEvidenceSourceCompletionV1::{Complete, Incomplete, Unsupported};
    290 
    291         assert_eq!(
    292             classify_trade_evidence_coverage_v1(
    293                 [source(true, Incomplete, 0), source(false, Unsupported, 0)],
    294                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    295             ),
    296             Ok(Missing)
    297         );
    298         assert_eq!(
    299             classify_trade_evidence_coverage_v1(
    300                 [source(true, Incomplete, 0), source(false, Complete, 0)],
    301                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    302             ),
    303             Ok(Partial)
    304         );
    305     }
    306 
    307     #[test]
    308     fn coverage_and_outcome_matrix_fails_closed() {
    309         use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
    310         use RadrootsTradeEvidenceOutcomeV1::{Indeterminate, Invalid, Valid};
    311 
    312         for coverage in [Missing, Partial, Unsupported] {
    313             assert!(coverage.permits(Indeterminate));
    314             assert!(!coverage.permits(Valid));
    315             assert!(!coverage.permits(Invalid));
    316         }
    317         for outcome in [Valid, Invalid, Indeterminate] {
    318             assert!(ScopeSatisfied.permits(outcome));
    319         }
    320         assert_eq!(RadrootsTradeEvidenceCoverageV1::default(), Missing);
    321         assert_eq!(RadrootsTradeEvidenceOutcomeV1::default(), Indeterminate);
    322     }
    323 
    324     #[test]
    325     fn source_and_event_bounds_are_enforced_before_unbounded_ingestion() {
    326         use RadrootsTradeEvidenceCoverageError::{
    327             AdmittedEventCountOutOfRange, NoRequiredSource, SourceCountOutOfRange,
    328         };
    329         use RadrootsTradeEvidenceSourceCompletionV1::Complete;
    330 
    331         let maximum = source(
    332             true,
    333             Complete,
    334             RADROOTS_TRADE_EVIDENCE_MAXIMUM_EVENTS_PER_SOURCE,
    335         );
    336         assert_eq!(maximum.admitted_event_count(), 4_096);
    337         assert_eq!(
    338             RadrootsTradeEvidenceSourceResultV1::new(
    339                 RadrootsTradeEvidenceSourceRequirementV1::Required,
    340                 Complete,
    341                 4_097,
    342             ),
    343             Err(AdmittedEventCountOutOfRange)
    344         );
    345 
    346         assert_eq!(
    347             classify_trade_evidence_coverage_v1(
    348                 core::iter::empty(),
    349                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    350             ),
    351             Err(SourceCountOutOfRange)
    352         );
    353         assert_eq!(
    354             classify_trade_evidence_coverage_v1(
    355                 [source(false, Complete, 0)],
    356                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    357             ),
    358             Err(NoRequiredSource)
    359         );
    360         assert_eq!(
    361             classify_trade_evidence_coverage_v1(
    362                 core::iter::repeat(source(true, Complete, 0)),
    363                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    364             ),
    365             Err(SourceCountOutOfRange)
    366         );
    367         assert_eq!(
    368             classify_trade_evidence_coverage_v1(
    369                 core::iter::repeat_n(
    370                     source(true, Complete, 0),
    371                     RADROOTS_TRADE_EVIDENCE_MAXIMUM_SOURCE_COUNT,
    372                 ),
    373                 RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    374             ),
    375             Ok(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied)
    376         );
    377     }
    378 
    379     #[cfg(feature = "json")]
    380     #[test]
    381     fn coverage_and_outcome_wire_values_are_exact() {
    382         use RadrootsTradeEvidenceCoverageV1::{Missing, Partial, ScopeSatisfied, Unsupported};
    383         use RadrootsTradeEvidenceOutcomeV1::{Indeterminate, Invalid, Valid};
    384 
    385         for (value, wire) in [
    386             (Missing, "\"missing\""),
    387             (Partial, "\"partial\""),
    388             (ScopeSatisfied, "\"scope_satisfied\""),
    389             (Unsupported, "\"unsupported\""),
    390         ] {
    391             assert_eq!(serde_json::to_string(&value).unwrap(), wire);
    392             assert_eq!(
    393                 serde_json::from_str::<RadrootsTradeEvidenceCoverageV1>(wire).unwrap(),
    394                 value
    395             );
    396         }
    397         for (value, wire) in [
    398             (Valid, "\"valid\""),
    399             (Invalid, "\"invalid\""),
    400             (Indeterminate, "\"indeterminate\""),
    401         ] {
    402             assert_eq!(serde_json::to_string(&value).unwrap(), wire);
    403             assert_eq!(
    404                 serde_json::from_str::<RadrootsTradeEvidenceOutcomeV1>(wire).unwrap(),
    405                 value
    406             );
    407         }
    408         for rejected in ["\"complete\"", "\"query_partial\"", "null", "{}"] {
    409             assert!(serde_json::from_str::<RadrootsTradeEvidenceCoverageV1>(rejected).is_err());
    410         }
    411     }
    412 
    413     #[test]
    414     fn coverage_errors_are_stable_and_source_free() {
    415         for (error, message) in [
    416             (
    417                 RadrootsTradeEvidenceCoverageError::SourceCountOutOfRange,
    418                 "evidence source count is out of range",
    419             ),
    420             (
    421                 RadrootsTradeEvidenceCoverageError::NoRequiredSource,
    422                 "evidence scope has no required source",
    423             ),
    424             (
    425                 RadrootsTradeEvidenceCoverageError::AdmittedEventCountOutOfRange,
    426                 "evidence source admitted event count is out of range",
    427             ),
    428         ] {
    429             assert_eq!(error.to_string(), message);
    430             assert!(!format!("{error:?}").contains("source_id"));
    431             #[cfg(feature = "std")]
    432             assert!(std::error::Error::source(&error).is_none());
    433         }
    434     }
    435 }