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 }