rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

reconciliation_attestation.rs (26115B)


      1 //! Canonical signed attestation construction from one finalization fence.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 use nostr::{EventBuilder, Kind, PublicKey as NostrPublicKey, Tag, Timestamp};
      7 use radroots_event::{
      8     envelope::EventEnvelope,
      9     id::EventId,
     10     wire::{EventWireLimits, Nip01EventWire},
     11 };
     12 use radroots_event_codec::{
     13     authoring::{AuthoredEventBody, AuthoredEventPlan},
     14     decode::rhi::{
     15         RadrootsRhiEvidenceAttestationV1, rhi_evidence_attestation_from_event,
     16         validate_rhi_evidence_attestation_supersession,
     17     },
     18 };
     19 use radroots_nostr::event::{Verification, verify, verify_id};
     20 use radroots_service_host::{EntropySource, UnixTimeSeconds};
     21 use radroots_trade::evidence::{
     22     RadrootsRhiEvidenceReasonCodeV1, RadrootsRhiEvidenceReportV1,
     23     RadrootsRhiEvidenceSupersessionV1, RadrootsTradeEvidenceProjectionDigestV1,
     24 };
     25 use sha2::{Digest, Sha256};
     26 use zeroize::Zeroizing;
     27 
     28 use crate::{
     29     RhiDecryptedIdentity, RhiReconciliationCoverage, RhiReconciliationFinalizationFence,
     30     RhiReconciliationOutcome, RhiReconciliationReasonCode,
     31 };
     32 
     33 /// Exact version of the signed reconciliation-attestation boundary.
     34 pub const RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION: u32 = 1;
     35 
     36 /// Exact cap for one canonical signed attestation event.
     37 pub const RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES: usize = 32 * 1024;
     38 
     39 const MAXIMUM_TAGS: usize = 7;
     40 const MAXIMUM_TAG_ELEMENTS: usize = 21;
     41 const MAXIMUM_TAG_ELEMENT_BYTES: usize = 128;
     42 const MAXIMUM_TAG_BYTES: usize = 1_024;
     43 
     44 /// Stable source-free signed-attestation failure class.
     45 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     46 pub enum RhiReconciliationAttestationErrorKind {
     47     InvalidInput,
     48     IdentityMismatch,
     49     EntropyUnavailable,
     50     ReportInvalid,
     51     EventPlanInvalid,
     52     SigningFailed,
     53     VerificationFailed,
     54     SupersessionInvalid,
     55 }
     56 
     57 impl RhiReconciliationAttestationErrorKind {
     58     /// Returns the stable machine-readable failure code.
     59     #[must_use]
     60     pub const fn code(self) -> &'static str {
     61         match self {
     62             Self::InvalidInput => "reconciliation_attestation_input_invalid",
     63             Self::IdentityMismatch => "reconciliation_attestation_identity_mismatch",
     64             Self::EntropyUnavailable => "reconciliation_attestation_entropy_unavailable",
     65             Self::ReportInvalid => "reconciliation_attestation_report_invalid",
     66             Self::EventPlanInvalid => "reconciliation_attestation_event_plan_invalid",
     67             Self::SigningFailed => "reconciliation_attestation_signing_failed",
     68             Self::VerificationFailed => "reconciliation_attestation_verification_failed",
     69             Self::SupersessionInvalid => "reconciliation_attestation_supersession_invalid",
     70         }
     71     }
     72 }
     73 
     74 /// Redacted source-free signed-attestation failure.
     75 #[derive(Clone, Copy, PartialEq, Eq)]
     76 pub struct RhiReconciliationAttestationError {
     77     kind: RhiReconciliationAttestationErrorKind,
     78 }
     79 
     80 impl RhiReconciliationAttestationError {
     81     /// Returns the stable failure class.
     82     #[must_use]
     83     pub const fn kind(self) -> RhiReconciliationAttestationErrorKind {
     84         self.kind
     85     }
     86 
     87     /// Returns the stable machine-readable failure code.
     88     #[must_use]
     89     pub const fn code(self) -> &'static str {
     90         self.kind.code()
     91     }
     92 }
     93 
     94 impl fmt::Display for RhiReconciliationAttestationError {
     95     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     96         formatter.write_str(match self.kind {
     97             RhiReconciliationAttestationErrorKind::InvalidInput => {
     98                 "RHI reconciliation attestation input is invalid"
     99             }
    100             RhiReconciliationAttestationErrorKind::IdentityMismatch => {
    101                 "RHI reconciliation attestation identity does not match"
    102             }
    103             RhiReconciliationAttestationErrorKind::EntropyUnavailable => {
    104                 "RHI reconciliation attestation entropy is unavailable"
    105             }
    106             RhiReconciliationAttestationErrorKind::ReportInvalid => {
    107                 "RHI reconciliation attestation report is invalid"
    108             }
    109             RhiReconciliationAttestationErrorKind::EventPlanInvalid => {
    110                 "RHI reconciliation attestation event plan is invalid"
    111             }
    112             RhiReconciliationAttestationErrorKind::SigningFailed => {
    113                 "RHI reconciliation attestation signing failed"
    114             }
    115             RhiReconciliationAttestationErrorKind::VerificationFailed => {
    116                 "RHI reconciliation attestation verification failed"
    117             }
    118             RhiReconciliationAttestationErrorKind::SupersessionInvalid => {
    119                 "RHI reconciliation attestation supersession is invalid"
    120             }
    121         })
    122     }
    123 }
    124 
    125 impl fmt::Debug for RhiReconciliationAttestationError {
    126     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    127         formatter
    128             .debug_struct("RhiReconciliationAttestationError")
    129             .field("kind", &self.kind)
    130             .finish()
    131     }
    132 }
    133 
    134 impl Error for RhiReconciliationAttestationError {}
    135 
    136 /// Sealed exact reference to one prior independently verified attestation.
    137 ///
    138 /// Callers cannot supply independent report and event identifiers that were
    139 /// never proven together.
    140 ///
    141 /// ```compile_fail
    142 /// use rhi::RhiEvidenceAttestationSupersession;
    143 ///
    144 /// let _forged = RhiEvidenceAttestationSupersession {};
    145 /// ```
    146 pub struct RhiEvidenceAttestationSupersession {
    147     report: RadrootsRhiEvidenceReportV1,
    148     event_id: EventId,
    149 }
    150 
    151 impl RhiEvidenceAttestationSupersession {
    152     /// Derives the only public supersession input from one verified result.
    153     #[must_use]
    154     pub fn from_attestation(attestation: &RhiSignedEvidenceAttestation) -> Self {
    155         Self {
    156             report: attestation.report.clone(),
    157             event_id: EventId::from_bytes(attestation.event_id),
    158         }
    159     }
    160 
    161     #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    162     pub(crate) fn from_persisted(
    163         trade_id: &radroots_event::id::TradeId,
    164         statement_sha256: [u8; 32],
    165         event_id: [u8; 32],
    166         canonical_report: &[u8],
    167         canonical_event_json: &[u8],
    168     ) -> Result<Self, RhiReconciliationAttestationError> {
    169         let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_report)
    170             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    171         if report.trade_id() != trade_id
    172             || report.statement_digest().as_bytes() != &statement_sha256
    173         {
    174             return Err(failure(
    175                 RhiReconciliationAttestationErrorKind::SupersessionInvalid,
    176             ));
    177         }
    178         let source = core::str::from_utf8(canonical_event_json)
    179             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    180         let wire = Nip01EventWire::parse_json_unverified_with_limits(source, signed_event_limits())
    181             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    182         let event = wire
    183             .into_unverified_envelope()
    184             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    185         if verify_id(&event) != Verification::IdVerified
    186             || verify(&event) != Verification::Verified
    187             || event.id().as_bytes() != &event_id
    188             || *event.author() != report.issuer_public_key()
    189         {
    190             return Err(failure(
    191                 RhiReconciliationAttestationErrorKind::SupersessionInvalid,
    192             ));
    193         }
    194         let typed = rhi_evidence_attestation_from_event(&event)
    195             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    196         if typed.canonical_content() != report.canonical_content() {
    197             return Err(failure(
    198                 RhiReconciliationAttestationErrorKind::SupersessionInvalid,
    199             ));
    200         }
    201         Ok(Self {
    202             report,
    203             event_id: EventId::from_bytes(event_id),
    204         })
    205     }
    206 }
    207 
    208 impl fmt::Debug for RhiEvidenceAttestationSupersession {
    209     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    210         formatter.write_str("RhiEvidenceAttestationSupersession([redacted])")
    211     }
    212 }
    213 
    214 /// Sealed exact report and independently verified signed event.
    215 ///
    216 /// This value owns its Step195 fence. Step199 consumes it, reruns the fence
    217 /// inside its final write transaction, and persists these exact bytes without
    218 /// rebuilding or re-signing them.
    219 ///
    220 /// ```compile_fail
    221 /// use rhi::RhiSignedEvidenceAttestation;
    222 ///
    223 /// let _forged = RhiSignedEvidenceAttestation { event_id: [0; 32] };
    224 /// ```
    225 ///
    226 /// ```compile_fail
    227 /// use rhi::RhiSignedEvidenceAttestation;
    228 ///
    229 /// fn require_clone<T: Clone>() {}
    230 /// require_clone::<RhiSignedEvidenceAttestation>();
    231 /// ```
    232 pub struct RhiSignedEvidenceAttestation {
    233     fence: RhiReconciliationFinalizationFence,
    234     report: RadrootsRhiEvidenceReportV1,
    235     event_id: [u8; 32],
    236     signed_event_bytes: Box<[u8]>,
    237     signed_event_sha256: [u8; 32],
    238     created_at_unix_seconds: u64,
    239 }
    240 
    241 impl RhiSignedEvidenceAttestation {
    242     /// Returns the exact RHI signed-attestation contract version.
    243     #[must_use]
    244     pub const fn contract_version(&self) -> u32 {
    245         RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION
    246     }
    247 
    248     /// Returns the exact canonical report bytes.
    249     #[must_use]
    250     pub fn canonical_report_bytes(&self) -> &[u8] {
    251         self.report.canonical_content().as_bytes()
    252     }
    253 
    254     /// Returns the domain-separated statement/report identifier.
    255     #[must_use]
    256     pub fn statement_digest(&self) -> [u8; 32] {
    257         *self.report.statement_digest().as_bytes()
    258     }
    259 
    260     /// Returns the independently verified NIP-01 event identifier.
    261     #[must_use]
    262     pub const fn event_id(&self) -> &[u8; 32] {
    263         &self.event_id
    264     }
    265 
    266     /// Returns the exact signed event bytes that later persistence must retain.
    267     #[must_use]
    268     pub fn signed_event_bytes(&self) -> &[u8] {
    269         &self.signed_event_bytes
    270     }
    271 
    272     /// Returns the SHA-256 digest of the exact signed event bytes.
    273     #[must_use]
    274     pub const fn signed_event_sha256(&self) -> &[u8; 32] {
    275         &self.signed_event_sha256
    276     }
    277 
    278     /// Returns the injected NIP-01 authored time, distinct from observation time.
    279     #[must_use]
    280     pub const fn created_at_unix_seconds(&self) -> u64 {
    281         self.created_at_unix_seconds
    282     }
    283 
    284     /// Returns the exact evidence coverage retained by the finalization chain.
    285     #[must_use]
    286     pub const fn coverage(&self) -> RhiReconciliationCoverage {
    287         self.fence.evaluation().coverage()
    288     }
    289 
    290     /// Returns the exact claim outcome retained by the finalization chain.
    291     #[must_use]
    292     pub const fn outcome(&self) -> RhiReconciliationOutcome {
    293         self.fence.evaluation().outcome()
    294     }
    295 
    296     /// Returns whether this report explicitly supersedes one verified predecessor.
    297     #[must_use]
    298     pub const fn has_supersession(&self) -> bool {
    299         self.report.supersession().is_some()
    300     }
    301 
    302     pub(crate) const fn fence(&self) -> &RhiReconciliationFinalizationFence {
    303         &self.fence
    304     }
    305 
    306     pub(crate) const fn issuer_public_key_bytes(&self) -> [u8; 32] {
    307         self.report.issuer_public_key().into_bytes()
    308     }
    309 
    310     pub(crate) const fn claim_mutation_id_bytes(&self) -> [u8; 32] {
    311         *self.report.claim_mutation_id().as_bytes()
    312     }
    313 
    314     pub(crate) const fn report_observed_at_unix_seconds(&self) -> u64 {
    315         self.report.observed_at_unix_s()
    316     }
    317 
    318     pub(crate) fn supersession_bytes(&self) -> Option<([u8; 32], [u8; 32])> {
    319         self.report
    320             .supersession()
    321             .map(|value| (*value.report_id().as_bytes(), *value.event_id().as_bytes()))
    322     }
    323 }
    324 
    325 impl fmt::Debug for RhiSignedEvidenceAttestation {
    326     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    327         formatter
    328             .debug_struct("RhiSignedEvidenceAttestation")
    329             .field("coverage", &self.coverage())
    330             .field("outcome", &self.outcome())
    331             .field("has_supersession", &self.has_supersession())
    332             .field("signed_event_bytes", &self.signed_event_bytes.len())
    333             .finish_non_exhaustive()
    334     }
    335 }
    336 
    337 /// Builds, signs, and independently verifies one exact evidence attestation.
    338 ///
    339 /// Signing consumes exactly 32 bytes from the injected entropy source. No
    340 /// clock, entropy, source, relay, persistence, task, or network authority is
    341 /// acquired implicitly.
    342 pub fn build_rhi_signed_evidence_attestation(
    343     fence: RhiReconciliationFinalizationFence,
    344     identity: &RhiDecryptedIdentity,
    345     created_at: UnixTimeSeconds,
    346     entropy: &dyn EntropySource,
    347     supersession: Option<RhiEvidenceAttestationSupersession>,
    348 ) -> Result<RhiSignedEvidenceAttestation, RhiReconciliationAttestationError> {
    349     let evaluation = fence.evaluation();
    350     let projection = evaluation.projection();
    351     let manifest = projection.manifest();
    352     let projection_digest = projection
    353         .digest()
    354         .ok_or_else(|| failure(RhiReconciliationAttestationErrorKind::InvalidInput))?;
    355     let issuer = identity
    356         .public_identity()
    357         .as_hex()
    358         .parse()
    359         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::IdentityMismatch))?;
    360     let reason_codes = evaluation
    361         .reason_codes()
    362         .iter()
    363         .copied()
    364         .map(report_reason)
    365         .collect::<Result<Vec<_>, _>>()?;
    366     let shared_supersession = supersession.as_ref().map(|prior| {
    367         RadrootsRhiEvidenceSupersessionV1::new(prior.report.statement_digest(), prior.event_id)
    368     });
    369     let report = RadrootsRhiEvidenceReportV1::new(
    370         issuer,
    371         *evaluation.claim_mutation_id(),
    372         evaluation.outcome(),
    373         reason_codes,
    374         RadrootsTradeEvidenceProjectionDigestV1::from_bytes(projection_digest),
    375         manifest.inner(),
    376         shared_supersession,
    377     )
    378     .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?;
    379     report
    380         .validate_against_manifest(manifest.inner())
    381         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?;
    382     let attestation =
    383         RadrootsRhiEvidenceAttestationV1::from_canonical_content(report.canonical_content())
    384             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?;
    385     if let Some(prior) = supersession.as_ref() {
    386         let current = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    387             prior.report.canonical_content(),
    388         )
    389         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    390         validate_rhi_evidence_attestation_supersession(&current, &prior.event_id, &attestation)
    391             .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?;
    392     }
    393     let body = AuthoredEventBody::from_rhi_evidence_attestation(&attestation)
    394         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?;
    395     let plan = AuthoredEventPlan::bind(body, created_at.get(), identity.public_identity().as_hex())
    396         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?;
    397 
    398     let mut auxiliary = Zeroizing::new([0_u8; 32]);
    399     entropy
    400         .fill_bytes(&mut auxiliary[..])
    401         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EntropyUnavailable))?;
    402     let signed_event = sign_plan(identity, &plan, &auxiliary)?;
    403     let signed_event_bytes = serde_json::to_vec(&signed_event)
    404         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SigningFailed))?;
    405     if signed_event_bytes.len() > RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES {
    406         return Err(failure(
    407             RhiReconciliationAttestationErrorKind::SigningFailed,
    408         ));
    409     }
    410     let verified = verify_signed_event(&plan, &report, manifest.inner(), &signed_event_bytes)?;
    411     let signed_event_sha256 = Sha256::digest(&signed_event_bytes).into();
    412     Ok(RhiSignedEvidenceAttestation {
    413         fence,
    414         report,
    415         event_id: *verified.id().as_bytes(),
    416         signed_event_bytes: signed_event_bytes.into_boxed_slice(),
    417         signed_event_sha256,
    418         created_at_unix_seconds: created_at.get(),
    419     })
    420 }
    421 
    422 fn report_reason(
    423     reason: RhiReconciliationReasonCode,
    424 ) -> Result<RadrootsRhiEvidenceReasonCodeV1, RhiReconciliationAttestationError> {
    425     RadrootsRhiEvidenceReasonCodeV1::parse(reason.code())
    426         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))
    427 }
    428 
    429 fn sign_plan(
    430     identity: &RhiDecryptedIdentity,
    431     plan: &AuthoredEventPlan,
    432     auxiliary: &[u8; 32],
    433 ) -> Result<nostr::Event, RhiReconciliationAttestationError> {
    434     let kind = u16::try_from(plan.body().kind())
    435         .map(Kind::Custom)
    436         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?;
    437     let tags = plan
    438         .body()
    439         .tags()
    440         .iter()
    441         .cloned()
    442         .map(Tag::parse)
    443         .collect::<Result<Vec<_>, _>>()
    444         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?;
    445     let author = NostrPublicKey::from_hex(identity.public_identity().as_hex())
    446         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::IdentityMismatch))?;
    447     let unsigned = EventBuilder::new(kind, plan.body().content())
    448         .tags(tags)
    449         .custom_created_at(Timestamp::from_secs(plan.created_at()))
    450         .build(author);
    451     if unsigned.id.as_ref().map(|event_id| event_id.to_bytes())
    452         != Some(*plan.expected_event_id().as_bytes())
    453     {
    454         return Err(failure(
    455             RhiReconciliationAttestationErrorKind::EventPlanInvalid,
    456         ));
    457     }
    458     identity
    459         .sign_nostr_event(unsigned, auxiliary)
    460         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SigningFailed))
    461 }
    462 
    463 fn verify_signed_event(
    464     plan: &AuthoredEventPlan,
    465     report: &RadrootsRhiEvidenceReportV1,
    466     manifest: &radroots_trade::evidence::RadrootsTradeEvidenceManifestV1,
    467     signed_event_bytes: &[u8],
    468 ) -> Result<EventEnvelope, RhiReconciliationAttestationError> {
    469     let event = verify_signed_event_plan(plan, signed_event_bytes)?;
    470     let typed = rhi_evidence_attestation_from_event(&event)
    471         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    472     if typed.canonical_content() != report.canonical_content() {
    473         return Err(failure(
    474             RhiReconciliationAttestationErrorKind::VerificationFailed,
    475         ));
    476     }
    477     let reparsed = RadrootsRhiEvidenceReportV1::from_canonical_content(event.content().as_bytes())
    478         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    479     reparsed
    480         .validate_against_manifest(manifest)
    481         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    482     if &reparsed != report {
    483         return Err(failure(
    484             RhiReconciliationAttestationErrorKind::VerificationFailed,
    485         ));
    486     }
    487     Ok(event)
    488 }
    489 
    490 fn verify_signed_event_plan(
    491     plan: &AuthoredEventPlan,
    492     signed_event_bytes: &[u8],
    493 ) -> Result<EventEnvelope, RhiReconciliationAttestationError> {
    494     if signed_event_bytes.is_empty()
    495         || signed_event_bytes.len() > RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES
    496     {
    497         return Err(failure(
    498             RhiReconciliationAttestationErrorKind::VerificationFailed,
    499         ));
    500     }
    501     let source = core::str::from_utf8(signed_event_bytes)
    502         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    503     let wire = Nip01EventWire::parse_json_unverified_with_limits(source, signed_event_limits())
    504         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    505     let event = wire
    506         .into_unverified_envelope()
    507         .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?;
    508     if verify_id(&event) != Verification::IdVerified || verify(&event) != Verification::Verified {
    509         return Err(failure(
    510             RhiReconciliationAttestationErrorKind::VerificationFailed,
    511         ));
    512     }
    513     if event.id().as_bytes() != plan.expected_event_id().as_bytes()
    514         || event.author().to_hex() != plan.author().to_hex()
    515         || event.created_at_u64() != plan.created_at()
    516         || event.kind_u32() != plan.body().kind()
    517         || event.tags_as_vec() != plan.body().tags()
    518         || event.content() != plan.body().content()
    519     {
    520         return Err(failure(
    521             RhiReconciliationAttestationErrorKind::VerificationFailed,
    522         ));
    523     }
    524     Ok(event)
    525 }
    526 
    527 const fn signed_event_limits() -> EventWireLimits {
    528     EventWireLimits {
    529         max_raw_json_bytes: RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES,
    530         max_content_bytes:
    531             radroots_trade::evidence::RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_CANONICAL_BYTES,
    532         max_tag_count: MAXIMUM_TAGS,
    533         max_total_tag_elements: MAXIMUM_TAG_ELEMENTS,
    534         max_tag_element_bytes: MAXIMUM_TAG_ELEMENT_BYTES,
    535         max_total_tag_bytes: MAXIMUM_TAG_BYTES,
    536         max_extra_fields: 0,
    537         max_total_extra_json_bytes: 0,
    538     }
    539 }
    540 
    541 const fn failure(kind: RhiReconciliationAttestationErrorKind) -> RhiReconciliationAttestationError {
    542     RhiReconciliationAttestationError { kind }
    543 }
    544 
    545 #[cfg(test)]
    546 mod tests {
    547     use super::*;
    548 
    549     const SIGNED_VECTOR: &str = include_str!(
    550         "../contracts/conformance/vectors/reconciliation_attestation_signed_event.v1.json"
    551     );
    552 
    553     fn vector_plan() -> AuthoredEventPlan {
    554         let value: serde_json::Value = serde_json::from_str(SIGNED_VECTOR).expect("signed vector");
    555         let content = value["content"].as_str().expect("report content");
    556         let attestation =
    557             RadrootsRhiEvidenceAttestationV1::from_canonical_content(content.as_bytes())
    558                 .expect("typed report");
    559         AuthoredEventPlan::bind(
    560             AuthoredEventBody::from_rhi_evidence_attestation(&attestation).expect("typed body"),
    561             value["created_at"].as_u64().expect("authored time"),
    562             value["pubkey"].as_str().expect("author"),
    563         )
    564         .expect("typed plan")
    565     }
    566 
    567     #[test]
    568     fn frozen_vector_and_malicious_signer_output_are_independently_verified() {
    569         let plan = vector_plan();
    570         verify_signed_event_plan(&plan, SIGNED_VECTOR.trim_end().as_bytes())
    571             .expect("frozen signed vector");
    572 
    573         let mut wrong_signature: serde_json::Value =
    574             serde_json::from_str(SIGNED_VECTOR).expect("signed vector");
    575         wrong_signature["sig"] = serde_json::Value::String("0".repeat(128));
    576         let bytes = serde_json::to_vec(&wrong_signature).expect("malicious signer wire");
    577         assert_eq!(
    578             verify_signed_event_plan(&plan, &bytes)
    579                 .expect_err("malicious signature")
    580                 .kind(),
    581             RhiReconciliationAttestationErrorKind::VerificationFailed
    582         );
    583 
    584         let mut wrong_author: serde_json::Value =
    585             serde_json::from_str(SIGNED_VECTOR).expect("signed vector");
    586         wrong_author["pubkey"] = serde_json::Value::String("2".repeat(64));
    587         let bytes = serde_json::to_vec(&wrong_author).expect("wrong-author wire");
    588         assert_eq!(
    589             verify_signed_event_plan(&plan, &bytes)
    590                 .expect_err("wrong author")
    591                 .kind(),
    592             RhiReconciliationAttestationErrorKind::VerificationFailed
    593         );
    594     }
    595 
    596     #[test]
    597     fn persisted_supersession_revalidates_canonical_report_and_signature() {
    598         let value: serde_json::Value = serde_json::from_str(SIGNED_VECTOR).expect("signed vector");
    599         let content = value["content"].as_str().expect("report content");
    600         let report = RadrootsRhiEvidenceReportV1::from_canonical_content(content.as_bytes())
    601             .expect("canonical report");
    602         let event = verify_signed_event_plan(&vector_plan(), SIGNED_VECTOR.trim_end().as_bytes())
    603             .expect("verified event");
    604         let supersession = RhiEvidenceAttestationSupersession::from_persisted(
    605             report.trade_id(),
    606             *report.statement_digest().as_bytes(),
    607             *event.id().as_bytes(),
    608             content.as_bytes(),
    609             SIGNED_VECTOR.trim_end().as_bytes(),
    610         )
    611         .expect("verified persisted supersession");
    612         assert_eq!(
    613             format!("{supersession:?}"),
    614             "RhiEvidenceAttestationSupersession([redacted])"
    615         );
    616 
    617         for (statement, event_id) in [
    618             ([0; 32], *event.id().as_bytes()),
    619             (*report.statement_digest().as_bytes(), [0; 32]),
    620         ] {
    621             assert_eq!(
    622                 RhiEvidenceAttestationSupersession::from_persisted(
    623                     report.trade_id(),
    624                     statement,
    625                     event_id,
    626                     content.as_bytes(),
    627                     SIGNED_VECTOR.trim_end().as_bytes(),
    628                 )
    629                 .expect_err("mismatched persisted identity")
    630                 .kind(),
    631                 RhiReconciliationAttestationErrorKind::SupersessionInvalid
    632             );
    633         }
    634     }
    635 
    636     #[test]
    637     fn every_public_error_class_is_source_free_and_redacted() {
    638         for kind in [
    639             RhiReconciliationAttestationErrorKind::InvalidInput,
    640             RhiReconciliationAttestationErrorKind::IdentityMismatch,
    641             RhiReconciliationAttestationErrorKind::EntropyUnavailable,
    642             RhiReconciliationAttestationErrorKind::ReportInvalid,
    643             RhiReconciliationAttestationErrorKind::EventPlanInvalid,
    644             RhiReconciliationAttestationErrorKind::SigningFailed,
    645             RhiReconciliationAttestationErrorKind::VerificationFailed,
    646             RhiReconciliationAttestationErrorKind::SupersessionInvalid,
    647         ] {
    648             let error = failure(kind);
    649             assert_eq!(error.kind(), kind);
    650             assert!(error.code().starts_with("reconciliation_attestation_"));
    651             assert!(Error::source(&error).is_none());
    652             let rendered = format!("{error} {error:?}");
    653             assert!(!rendered.contains("11111111"));
    654             assert!(!rendered.contains("f1a2a41d"));
    655             assert!(!rendered.contains("1b84c556"));
    656         }
    657     }
    658 }