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 }