Skip to main content

chio_federation/
lib.rs

1//! Chio federated trust, quorum, and shared reputation contracts.
2//!
3//! These contracts extend Chio's local listing, governance, and open-market
4//! surfaces into one bounded cross-operator federation lane. Federation stays
5//! evidence-referential and fail-closed: visibility may flow across operators,
6//! but runtime trust still requires explicit local activation and review.
7
8pub use chio_core_types::{capability, receipt};
9pub use chio_listing as listing;
10pub use chio_open_market as open_market;
11
12pub mod bilateral;
13pub mod trust_establishment;
14
15pub use bilateral::{
16    co_sign_with_origin, BilateralCoSigningError, BilateralCoSigningProtocol, CoSigningBody,
17    CoSigningRequest, CoSigningResponse, DualSignedReceipt, InProcessCoSigner,
18    BILATERAL_COSIGNING_SCHEMA, BILATERAL_DUAL_RECEIPT_SCHEMA,
19};
20pub use trust_establishment::{
21    FederationPeer, FederationPeerStore, HandshakeChallenge, InMemoryPeerStore,
22    KernelTrustExchange, KernelTrustExchangeConfig, PeerHandshakeEnvelope, PeerHandshakeError,
23    DEFAULT_HANDSHAKE_MAX_SKEW_SECS, DEFAULT_ROTATION_WINDOW_SECS, FEDERATION_HANDSHAKE_SCHEMA,
24};
25
26use std::collections::HashSet;
27
28use serde::{Deserialize, Serialize};
29
30use crate::capability::MonetaryAmount;
31use crate::listing::{
32    GenericListingActorKind, GenericListingFreshnessState, GenericListingReplicaFreshness,
33    GenericRegistryPublisher, GenericRegistryPublisherRole, GenericTrustAdmissionClass,
34};
35use crate::open_market::OpenMarketBondClass;
36use crate::receipt::SignedExportEnvelope;
37
38pub const CHIO_FEDERATION_ACTIVATION_EXCHANGE_SCHEMA: &str =
39    "chio.federation-activation-exchange.v1";
40pub const CHIO_FEDERATION_QUORUM_REPORT_SCHEMA: &str = "chio.federation-quorum-report.v1";
41pub const CHIO_FEDERATION_OPEN_ADMISSION_POLICY_SCHEMA: &str =
42    "chio.federation-open-admission-policy.v1";
43pub const CHIO_FEDERATION_REPUTATION_CLEARING_SCHEMA: &str =
44    "chio.federation-reputation-clearing.v1";
45pub const CHIO_FEDERATION_QUALIFICATION_MATRIX_SCHEMA: &str =
46    "chio.federation-qualification-matrix.v1";
47
48const FEDERATION_REQUIRED_REQUIREMENTS: [&str; 5] = [
49    "TRUSTMAX-01",
50    "TRUSTMAX-02",
51    "TRUSTMAX-03",
52    "TRUSTMAX-04",
53    "TRUSTMAX-05",
54];
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum FederationArtifactKind {
59    TrustActivation,
60    Listing,
61    ListingReport,
62    GovernanceCharter,
63    GovernanceCase,
64    OpenMarketFeeSchedule,
65    OpenMarketPenalty,
66    PortableReputationSummary,
67    PortableNegativeEvent,
68    CrossIssuerTrustPack,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum FederationQuorumState {
74    Converged,
75    Stale,
76    Conflicting,
77    InsufficientQuorum,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum FederatedReputationInputKind {
83    ReputationSummary,
84    NegativeEvent,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum FederationScenarioKind {
90    HostilePublisher,
91    ConflictingActivation,
92    InsufficientQuorum,
93    EclipseAttempt,
94    ReputationSybil,
95    GovernanceInterop,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum FederationQualificationOutcome {
101    Pass,
102    FailClosed,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase", deny_unknown_fields)]
107pub struct FederationArtifactReference {
108    pub kind: FederationArtifactKind,
109    pub schema: String,
110    pub artifact_id: String,
111    pub operator_id: String,
112    pub sha256: String,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub uri: Option<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase", deny_unknown_fields)]
119pub struct FederationTrustScope {
120    pub namespace: String,
121    pub subject_operator_id: String,
122    pub allowed_actor_kinds: Vec<GenericListingActorKind>,
123    pub allowed_admission_classes: Vec<GenericTrustAdmissionClass>,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub policy_reference: Option<String>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct FederationDelegationControl {
131    pub delegator_operator_id: String,
132    pub delegate_operator_id: String,
133    pub max_hops: u32,
134    pub attenuation_required: bool,
135    pub visibility_only_until_local_activation: bool,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase", deny_unknown_fields)]
140pub struct FederationImportControl {
141    pub explicit_local_activation_required: bool,
142    pub manual_review_required: bool,
143    pub reject_stale_inputs: bool,
144    pub allow_visibility_without_runtime_trust: bool,
145    pub prohibit_ambient_runtime_admission: bool,
146}
147
148impl Default for FederationImportControl {
149    fn default() -> Self {
150        Self {
151            explicit_local_activation_required: true,
152            manual_review_required: true,
153            reject_stale_inputs: true,
154            allow_visibility_without_runtime_trust: true,
155            prohibit_ambient_runtime_admission: true,
156        }
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct FederationActivationExchangeArtifact {
163    pub schema: String,
164    pub exchange_id: String,
165    pub issued_at: u64,
166    pub expires_at: u64,
167    pub source_operator_id: String,
168    pub target_operator_id: String,
169    pub listing_id: String,
170    pub activation_ref: FederationArtifactReference,
171    pub listing_ref: FederationArtifactReference,
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub governing_charter_ref: Option<FederationArtifactReference>,
174    pub scope: FederationTrustScope,
175    pub delegation_control: FederationDelegationControl,
176    pub import_control: FederationImportControl,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub note: Option<String>,
179}
180
181pub type SignedFederationActivationExchange =
182    SignedExportEnvelope<FederationActivationExchangeArtifact>;
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct FederationPublisherObservation {
187    pub publisher: GenericRegistryPublisher,
188    pub report_ref: FederationArtifactReference,
189    pub observed_listing_sha256: String,
190    pub freshness: GenericListingReplicaFreshness,
191    pub observed_at: u64,
192    pub upstream_hop_count: u32,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct FederationConflictEvidence {
198    pub divergence_key: String,
199    pub publisher_operator_ids: Vec<String>,
200    pub reason: String,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase", deny_unknown_fields)]
205pub struct FederationAntiEclipsePolicy {
206    pub minimum_distinct_operators: u32,
207    pub require_origin_publisher: bool,
208    pub require_indexer_observation: bool,
209    pub max_upstream_hops: u32,
210}
211
212impl Default for FederationAntiEclipsePolicy {
213    fn default() -> Self {
214        Self {
215            minimum_distinct_operators: 2,
216            require_origin_publisher: true,
217            require_indexer_observation: true,
218            max_upstream_hops: 1,
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase", deny_unknown_fields)]
225pub struct FederationQuorumReport {
226    pub schema: String,
227    pub report_id: String,
228    pub generated_at: u64,
229    pub namespace: String,
230    pub listing_id: String,
231    pub origin_operator_id: String,
232    pub quorum_threshold: u32,
233    pub max_replica_age_secs: u64,
234    pub publishers: Vec<FederationPublisherObservation>,
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    pub conflicts: Vec<FederationConflictEvidence>,
237    pub anti_eclipse_policy: FederationAntiEclipsePolicy,
238    pub final_state: FederationQuorumState,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub note: Option<String>,
241}
242
243pub type SignedFederationQuorumReport = SignedExportEnvelope<FederationQuorumReport>;
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase", deny_unknown_fields)]
247pub struct FederatedStakeRequirement {
248    pub admission_class: GenericTrustAdmissionClass,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub required_bond_class: Option<OpenMarketBondClass>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub minimum_bond_amount: Option<MonetaryAmount>,
253    pub slashable: bool,
254    pub governance_case_required: bool,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase", deny_unknown_fields)]
259pub struct FederatedOpenAdmissionPolicyArtifact {
260    pub schema: String,
261    pub policy_id: String,
262    pub issued_at: u64,
263    pub namespace: String,
264    pub governing_operator_id: String,
265    pub allowed_admission_classes: Vec<GenericTrustAdmissionClass>,
266    #[serde(default, skip_serializing_if = "Vec::is_empty")]
267    pub stake_requirements: Vec<FederatedStakeRequirement>,
268    pub governing_charter_ref: FederationArtifactReference,
269    pub fee_schedule_ref: FederationArtifactReference,
270    pub explicit_local_review_required: bool,
271    pub visibility_only_without_activation: bool,
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub note: Option<String>,
274}
275
276pub type SignedFederatedOpenAdmissionPolicy =
277    SignedExportEnvelope<FederatedOpenAdmissionPolicyArtifact>;
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase", deny_unknown_fields)]
281pub struct FederatedReputationInputReference {
282    pub kind: FederatedReputationInputKind,
283    pub artifact_ref: FederationArtifactReference,
284    pub subject_key: String,
285    pub issuer_operator_id: String,
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub issuer_independence_group_id: Option<String>,
288    pub weight_bps: u32,
289    pub blocking: bool,
290    pub published_at: u64,
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub expires_at: Option<u64>,
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub note: Option<String>,
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase", deny_unknown_fields)]
299pub struct FederatedSybilControl {
300    pub minimum_independent_issuers: u32,
301    pub maximum_inputs_per_issuer: u32,
302    pub oracle_cap_bps: u32,
303    pub local_weighting_required: bool,
304    pub negative_event_corroboration_required: bool,
305}
306
307impl Default for FederatedSybilControl {
308    fn default() -> Self {
309        Self {
310            minimum_independent_issuers: 2,
311            maximum_inputs_per_issuer: 2,
312            oracle_cap_bps: 4_000,
313            local_weighting_required: true,
314            negative_event_corroboration_required: true,
315        }
316    }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase", deny_unknown_fields)]
321pub struct FederatedReputationClearingContinuity {
322    pub continuity_id: String,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub previous_clearing_id: Option<String>,
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase", deny_unknown_fields)]
329pub struct FederatedReputationClearingArtifact {
330    pub schema: String,
331    pub clearing_id: String,
332    pub generated_at: u64,
333    pub subject_key: String,
334    pub namespace: String,
335    pub participating_operator_ids: Vec<String>,
336    pub local_weighting_policy_ref: String,
337    pub admission_policy_ref: String,
338    pub inputs: Vec<FederatedReputationInputReference>,
339    pub sybil_control: FederatedSybilControl,
340    pub accepted_input_ids: Vec<String>,
341    pub rejected_input_ids: Vec<String>,
342    pub effective_admission_class: GenericTrustAdmissionClass,
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub continuity: Option<FederatedReputationClearingContinuity>,
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub note: Option<String>,
347}
348
349pub type SignedFederatedReputationClearing =
350    SignedExportEnvelope<FederatedReputationClearingArtifact>;
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase", deny_unknown_fields)]
354pub struct FederationQualificationCase {
355    pub id: String,
356    pub name: String,
357    pub requirement_ids: Vec<String>,
358    pub scenario: FederationScenarioKind,
359    pub expected_outcome: FederationQualificationOutcome,
360    pub observed_outcome: FederationQualificationOutcome,
361    pub notes: String,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase", deny_unknown_fields)]
366pub struct FederationQualificationMatrix {
367    pub schema: String,
368    pub profile_id: String,
369    pub exchange_ref: String,
370    pub quorum_report_ref: String,
371    pub reputation_clearing_ref: String,
372    pub cases: Vec<FederationQualificationCase>,
373}
374
375pub type SignedFederationQualificationMatrix = SignedExportEnvelope<FederationQualificationMatrix>;
376
377#[derive(Debug, thiserror::Error, PartialEq, Eq)]
378pub enum FederationContractError {
379    #[error("unsupported schema: {0}")]
380    UnsupportedSchema(String),
381
382    #[error("missing field: {0}")]
383    MissingField(&'static str),
384
385    #[error("duplicate value: {0}")]
386    DuplicateValue(String),
387
388    #[error("invalid reference: {0}")]
389    InvalidReference(String),
390
391    #[error("invalid exchange: {0}")]
392    InvalidExchange(String),
393
394    #[error("invalid quorum: {0}")]
395    InvalidQuorum(String),
396
397    #[error("invalid admission: {0}")]
398    InvalidAdmission(String),
399
400    #[error("invalid clearing: {0}")]
401    InvalidClearing(String),
402
403    #[error("invalid qualification case: {0}")]
404    InvalidQualificationCase(String),
405}
406
407pub fn validate_federation_activation_exchange(
408    exchange: &FederationActivationExchangeArtifact,
409) -> Result<(), FederationContractError> {
410    if exchange.schema != CHIO_FEDERATION_ACTIVATION_EXCHANGE_SCHEMA {
411        return Err(FederationContractError::UnsupportedSchema(
412            exchange.schema.clone(),
413        ));
414    }
415    ensure_non_empty(&exchange.exchange_id, "federation_exchange.exchange_id")?;
416    ensure_non_empty(
417        &exchange.source_operator_id,
418        "federation_exchange.source_operator_id",
419    )?;
420    ensure_non_empty(
421        &exchange.target_operator_id,
422        "federation_exchange.target_operator_id",
423    )?;
424    ensure_non_empty(&exchange.listing_id, "federation_exchange.listing_id")?;
425    if exchange.source_operator_id == exchange.target_operator_id {
426        return Err(FederationContractError::InvalidExchange(
427            "source_operator_id and target_operator_id must differ".to_string(),
428        ));
429    }
430    if exchange.expires_at <= exchange.issued_at {
431        return Err(FederationContractError::InvalidExchange(
432            "expires_at must be greater than issued_at".to_string(),
433        ));
434    }
435    validate_federation_artifact_reference(
436        &exchange.activation_ref,
437        "federation_exchange.activation_ref",
438    )?;
439    validate_federation_artifact_reference(
440        &exchange.listing_ref,
441        "federation_exchange.listing_ref",
442    )?;
443    if exchange.activation_ref.kind != FederationArtifactKind::TrustActivation {
444        return Err(FederationContractError::InvalidExchange(
445            "activation_ref must reference a trust activation artifact".to_string(),
446        ));
447    }
448    if exchange.listing_ref.kind != FederationArtifactKind::Listing {
449        return Err(FederationContractError::InvalidExchange(
450            "listing_ref must reference a listing artifact".to_string(),
451        ));
452    }
453    if let Some(charter_ref) = exchange.governing_charter_ref.as_ref() {
454        validate_federation_artifact_reference(
455            charter_ref,
456            "federation_exchange.governing_charter_ref",
457        )?;
458        if charter_ref.kind != FederationArtifactKind::GovernanceCharter {
459            return Err(FederationContractError::InvalidExchange(
460                "governing_charter_ref must reference a governance charter".to_string(),
461            ));
462        }
463    }
464    validate_federation_scope(&exchange.scope)?;
465    validate_delegation_control(&exchange.delegation_control)?;
466    validate_import_control(&exchange.import_control)?;
467    if exchange.delegation_control.delegator_operator_id != exchange.source_operator_id {
468        return Err(FederationContractError::InvalidExchange(
469            "delegation_control.delegator_operator_id must match source_operator_id".to_string(),
470        ));
471    }
472    if exchange.delegation_control.delegate_operator_id != exchange.target_operator_id {
473        return Err(FederationContractError::InvalidExchange(
474            "delegation_control.delegate_operator_id must match target_operator_id".to_string(),
475        ));
476    }
477    Ok(())
478}
479
480pub fn validate_federation_quorum_report(
481    report: &FederationQuorumReport,
482) -> Result<(), FederationContractError> {
483    if report.schema != CHIO_FEDERATION_QUORUM_REPORT_SCHEMA {
484        return Err(FederationContractError::UnsupportedSchema(
485            report.schema.clone(),
486        ));
487    }
488    ensure_non_empty(&report.report_id, "federation_quorum.report_id")?;
489    ensure_non_empty(&report.namespace, "federation_quorum.namespace")?;
490    ensure_non_empty(&report.listing_id, "federation_quorum.listing_id")?;
491    ensure_non_empty(
492        &report.origin_operator_id,
493        "federation_quorum.origin_operator_id",
494    )?;
495    if report.quorum_threshold == 0 {
496        return Err(FederationContractError::InvalidQuorum(
497            "quorum_threshold must be non-zero".to_string(),
498        ));
499    }
500    if report.max_replica_age_secs == 0 {
501        return Err(FederationContractError::InvalidQuorum(
502            "max_replica_age_secs must be non-zero".to_string(),
503        ));
504    }
505    if report.publishers.is_empty() {
506        return Err(FederationContractError::MissingField(
507            "federation_quorum.publishers",
508        ));
509    }
510    validate_anti_eclipse_policy(&report.anti_eclipse_policy)?;
511
512    let mut publisher_ids = HashSet::new();
513    let mut fresh_count = 0_u32;
514    let mut stale_count = 0_u32;
515    let mut has_origin = false;
516    let mut has_indexer = false;
517    for publisher in &report.publishers {
518        publisher
519            .publisher
520            .validate()
521            .map_err(FederationContractError::InvalidQuorum)?;
522        validate_federation_artifact_reference(
523            &publisher.report_ref,
524            "federation_quorum.publishers.report_ref",
525        )?;
526        if publisher.report_ref.kind != FederationArtifactKind::ListingReport {
527            return Err(FederationContractError::InvalidQuorum(
528                "publisher report_ref must reference a listing report".to_string(),
529            ));
530        }
531        if publisher.report_ref.operator_id != publisher.publisher.operator_id {
532            return Err(FederationContractError::InvalidQuorum(
533                "publisher report_ref operator_id must match publisher.operator_id".to_string(),
534            ));
535        }
536        validate_hex_digest(
537            &publisher.observed_listing_sha256,
538            "federation_quorum.publishers.observed_listing_sha256",
539        )?;
540        publisher
541            .freshness
542            .validate()
543            .map_err(FederationContractError::InvalidQuorum)?;
544        if publisher.freshness.age_secs > report.max_replica_age_secs
545            || publisher.freshness.state == GenericListingFreshnessState::Stale
546        {
547            stale_count += 1;
548        } else {
549            fresh_count += 1;
550        }
551        if publisher.upstream_hop_count > report.anti_eclipse_policy.max_upstream_hops {
552            return Err(FederationContractError::InvalidQuorum(
553                "publisher upstream_hop_count exceeds anti-eclipse policy".to_string(),
554            ));
555        }
556        if !publisher_ids.insert(publisher.publisher.operator_id.as_str()) {
557            return Err(FederationContractError::DuplicateValue(
558                publisher.publisher.operator_id.clone(),
559            ));
560        }
561        if publisher.publisher.role == GenericRegistryPublisherRole::Origin
562            && publisher.publisher.operator_id == report.origin_operator_id
563        {
564            has_origin = true;
565        }
566        if publisher.publisher.role == GenericRegistryPublisherRole::Indexer {
567            has_indexer = true;
568        }
569    }
570
571    if report.anti_eclipse_policy.require_origin_publisher && !has_origin {
572        return Err(FederationContractError::InvalidQuorum(
573            "anti-eclipse policy requires an origin publisher observation".to_string(),
574        ));
575    }
576    if report.anti_eclipse_policy.require_indexer_observation && !has_indexer {
577        return Err(FederationContractError::InvalidQuorum(
578            "anti-eclipse policy requires an indexer observation".to_string(),
579        ));
580    }
581    if publisher_ids.len() < report.anti_eclipse_policy.minimum_distinct_operators as usize {
582        return Err(FederationContractError::InvalidQuorum(
583            "insufficient distinct operators for anti-eclipse policy".to_string(),
584        ));
585    }
586
587    let mut divergence_keys = HashSet::new();
588    for conflict in &report.conflicts {
589        ensure_non_empty(
590            &conflict.divergence_key,
591            "federation_quorum.conflicts.divergence_key",
592        )?;
593        ensure_non_empty(&conflict.reason, "federation_quorum.conflicts.reason")?;
594        ensure_unique_strings(
595            &conflict.publisher_operator_ids,
596            "federation_quorum.conflicts.publisher_operator_ids",
597        )?;
598        if !divergence_keys.insert(conflict.divergence_key.as_str()) {
599            return Err(FederationContractError::DuplicateValue(
600                conflict.divergence_key.clone(),
601            ));
602        }
603    }
604
605    match report.final_state {
606        FederationQuorumState::Converged => {
607            if !report.conflicts.is_empty() {
608                return Err(FederationContractError::InvalidQuorum(
609                    "converged quorum reports cannot include conflicts".to_string(),
610                ));
611            }
612            if fresh_count < report.quorum_threshold {
613                return Err(FederationContractError::InvalidQuorum(
614                    "converged quorum reports require fresh observations meeting the quorum threshold"
615                        .to_string(),
616                ));
617            }
618        }
619        FederationQuorumState::Conflicting => {
620            if report.conflicts.is_empty() {
621                return Err(FederationContractError::InvalidQuorum(
622                    "conflicting quorum reports require conflict evidence".to_string(),
623                ));
624            }
625        }
626        FederationQuorumState::InsufficientQuorum => {
627            if fresh_count >= report.quorum_threshold
628                && publisher_ids.len()
629                    >= report.anti_eclipse_policy.minimum_distinct_operators as usize
630                && (!report.anti_eclipse_policy.require_origin_publisher || has_origin)
631                && (!report.anti_eclipse_policy.require_indexer_observation || has_indexer)
632            {
633                return Err(FederationContractError::InvalidQuorum(
634                    "insufficient_quorum requires a real quorum shortfall".to_string(),
635                ));
636            }
637        }
638        FederationQuorumState::Stale => {
639            if stale_count != report.publishers.len() as u32 {
640                return Err(FederationContractError::InvalidQuorum(
641                    "stale quorum reports require all observations to be stale".to_string(),
642                ));
643            }
644        }
645    }
646
647    Ok(())
648}
649
650pub fn validate_federated_open_admission_policy(
651    policy: &FederatedOpenAdmissionPolicyArtifact,
652) -> Result<(), FederationContractError> {
653    if policy.schema != CHIO_FEDERATION_OPEN_ADMISSION_POLICY_SCHEMA {
654        return Err(FederationContractError::UnsupportedSchema(
655            policy.schema.clone(),
656        ));
657    }
658    ensure_non_empty(&policy.policy_id, "federated_admission.policy_id")?;
659    ensure_non_empty(&policy.namespace, "federated_admission.namespace")?;
660    ensure_non_empty(
661        &policy.governing_operator_id,
662        "federated_admission.governing_operator_id",
663    )?;
664    if policy.allowed_admission_classes.is_empty() {
665        return Err(FederationContractError::MissingField(
666            "federated_admission.allowed_admission_classes",
667        ));
668    }
669    ensure_unique_copy_values(
670        &policy.allowed_admission_classes,
671        "federated_admission.allowed_admission_classes",
672    )?;
673    validate_federation_artifact_reference(
674        &policy.governing_charter_ref,
675        "federated_admission.governing_charter_ref",
676    )?;
677    if policy.governing_charter_ref.kind != FederationArtifactKind::GovernanceCharter {
678        return Err(FederationContractError::InvalidAdmission(
679            "governing_charter_ref must reference a governance charter".to_string(),
680        ));
681    }
682    validate_federation_artifact_reference(
683        &policy.fee_schedule_ref,
684        "federated_admission.fee_schedule_ref",
685    )?;
686    if policy.fee_schedule_ref.kind != FederationArtifactKind::OpenMarketFeeSchedule {
687        return Err(FederationContractError::InvalidAdmission(
688            "fee_schedule_ref must reference an open-market fee schedule".to_string(),
689        ));
690    }
691    if !policy.explicit_local_review_required {
692        return Err(FederationContractError::InvalidAdmission(
693            "open admission must still require explicit local review".to_string(),
694        ));
695    }
696    if !policy.visibility_only_without_activation {
697        return Err(FederationContractError::InvalidAdmission(
698            "open admission must remain visibility-only without activation".to_string(),
699        ));
700    }
701
702    let mut requirement_classes = HashSet::new();
703    for requirement in &policy.stake_requirements {
704        validate_stake_requirement(requirement)?;
705        if !policy
706            .allowed_admission_classes
707            .contains(&requirement.admission_class)
708        {
709            return Err(FederationContractError::InvalidAdmission(
710                "stake requirement admission_class must be allowed by the policy".to_string(),
711            ));
712        }
713        if !requirement_classes.insert(requirement.admission_class) {
714            return Err(FederationContractError::DuplicateValue(format!(
715                "{:?}",
716                requirement.admission_class
717            )));
718        }
719    }
720
721    if policy
722        .allowed_admission_classes
723        .contains(&GenericTrustAdmissionClass::BondBacked)
724        && !requirement_classes.contains(&GenericTrustAdmissionClass::BondBacked)
725    {
726        return Err(FederationContractError::InvalidAdmission(
727            "bond_backed admission requires an explicit stake requirement".to_string(),
728        ));
729    }
730
731    Ok(())
732}
733
734pub fn validate_federated_reputation_clearing(
735    clearing: &FederatedReputationClearingArtifact,
736) -> Result<(), FederationContractError> {
737    if clearing.schema != CHIO_FEDERATION_REPUTATION_CLEARING_SCHEMA {
738        return Err(FederationContractError::UnsupportedSchema(
739            clearing.schema.clone(),
740        ));
741    }
742    ensure_non_empty(&clearing.clearing_id, "federated_clearing.clearing_id")?;
743    ensure_non_empty(&clearing.subject_key, "federated_clearing.subject_key")?;
744    ensure_non_empty(&clearing.namespace, "federated_clearing.namespace")?;
745    ensure_non_empty(
746        &clearing.local_weighting_policy_ref,
747        "federated_clearing.local_weighting_policy_ref",
748    )?;
749    ensure_non_empty(
750        &clearing.admission_policy_ref,
751        "federated_clearing.admission_policy_ref",
752    )?;
753    if clearing.participating_operator_ids.is_empty() {
754        return Err(FederationContractError::MissingField(
755            "federated_clearing.participating_operator_ids",
756        ));
757    }
758    ensure_unique_strings(
759        &clearing.participating_operator_ids,
760        "federated_clearing.participating_operator_ids",
761    )?;
762    validate_sybil_control(&clearing.sybil_control)?;
763    if let Some(continuity) = clearing.continuity.as_ref() {
764        validate_reputation_clearing_continuity(continuity, &clearing.clearing_id)?;
765    }
766    if clearing.inputs.is_empty() {
767        return Err(FederationContractError::MissingField(
768            "federated_clearing.inputs",
769        ));
770    }
771
772    let participating_operators = clearing
773        .participating_operator_ids
774        .iter()
775        .map(String::as_str)
776        .collect::<HashSet<_>>();
777    let mut input_ids = HashSet::new();
778    let mut accepted_ids = HashSet::new();
779    let mut rejected_ids = HashSet::new();
780    let mut issuer_counts = std::collections::BTreeMap::<String, u32>::new();
781    let mut accepted_summary_issuers = HashSet::new();
782    let mut accepted_independence_groups = HashSet::new();
783    let mut accepted_negative_event_groups = HashSet::new();
784
785    for id in &clearing.accepted_input_ids {
786        ensure_non_empty(id, "federated_clearing.accepted_input_ids")?;
787        if !accepted_ids.insert(id.as_str()) {
788            return Err(FederationContractError::DuplicateValue(id.clone()));
789        }
790    }
791    for id in &clearing.rejected_input_ids {
792        ensure_non_empty(id, "federated_clearing.rejected_input_ids")?;
793        if !rejected_ids.insert(id.as_str()) {
794            return Err(FederationContractError::DuplicateValue(id.clone()));
795        }
796    }
797    if accepted_ids.iter().any(|id| rejected_ids.contains(id)) {
798        return Err(FederationContractError::InvalidClearing(
799            "an input cannot be both accepted and rejected".to_string(),
800        ));
801    }
802
803    for input in &clearing.inputs {
804        validate_reputation_input_reference(input, clearing.generated_at, &clearing.subject_key)?;
805        if input.artifact_ref.operator_id != input.issuer_operator_id {
806            return Err(FederationContractError::InvalidClearing(
807                "input artifact_ref operator_id must match issuer_operator_id".to_string(),
808            ));
809        }
810        if !participating_operators.contains(input.issuer_operator_id.as_str()) {
811            return Err(FederationContractError::InvalidClearing(
812                "input issuer_operator_id must appear in participating_operator_ids".to_string(),
813            ));
814        }
815        let input_id = input.artifact_ref.artifact_id.as_str();
816        if !input_ids.insert(input_id) {
817            return Err(FederationContractError::DuplicateValue(
818                input.artifact_ref.artifact_id.clone(),
819            ));
820        }
821        *issuer_counts
822            .entry(input.issuer_operator_id.clone())
823            .or_insert(0) += 1;
824        if issuer_counts[&input.issuer_operator_id]
825            > clearing.sybil_control.maximum_inputs_per_issuer
826        {
827            return Err(FederationContractError::InvalidClearing(
828                "issuer exceeds maximum_inputs_per_issuer".to_string(),
829            ));
830        }
831        if input.weight_bps > clearing.sybil_control.oracle_cap_bps {
832            return Err(FederationContractError::InvalidClearing(
833                "input weight_bps exceeds oracle_cap_bps".to_string(),
834            ));
835        }
836        if accepted_ids.contains(input_id) {
837            let independence_group = input
838                .issuer_independence_group_id
839                .as_deref()
840                .unwrap_or(input.issuer_operator_id.as_str());
841            accepted_independence_groups.insert(independence_group);
842            match input.kind {
843                FederatedReputationInputKind::ReputationSummary => {
844                    if !accepted_summary_issuers.insert(input.issuer_operator_id.as_str()) {
845                        return Err(FederationContractError::InvalidClearing(
846                            "accepted reputation summaries must come from distinct issuers"
847                                .to_string(),
848                        ));
849                    }
850                }
851                FederatedReputationInputKind::NegativeEvent => {
852                    if input.blocking {
853                        accepted_negative_event_groups.insert(independence_group);
854                    }
855                }
856            }
857        }
858    }
859
860    if input_ids.len() != clearing.accepted_input_ids.len() + clearing.rejected_input_ids.len() {
861        return Err(FederationContractError::InvalidClearing(
862            "each input must be classified as accepted or rejected".to_string(),
863        ));
864    }
865    if accepted_independence_groups.len()
866        < clearing.sybil_control.minimum_independent_issuers as usize
867    {
868        return Err(FederationContractError::InvalidClearing(
869            "accepted inputs must meet the minimum_independent_issuers threshold".to_string(),
870        ));
871    }
872    if clearing.sybil_control.negative_event_corroboration_required
873        && accepted_negative_event_groups.len() == 1
874    {
875        return Err(FederationContractError::InvalidClearing(
876            "blocking negative events require corroboration from independent issuers".to_string(),
877        ));
878    }
879    if clearing.effective_admission_class != GenericTrustAdmissionClass::PublicUntrusted
880        && clearing.accepted_input_ids.is_empty()
881    {
882        return Err(FederationContractError::InvalidClearing(
883            "non-public admission classes require accepted inputs".to_string(),
884        ));
885    }
886
887    Ok(())
888}
889
890pub fn validate_federation_qualification_matrix(
891    matrix: &FederationQualificationMatrix,
892) -> Result<(), FederationContractError> {
893    if matrix.schema != CHIO_FEDERATION_QUALIFICATION_MATRIX_SCHEMA {
894        return Err(FederationContractError::UnsupportedSchema(
895            matrix.schema.clone(),
896        ));
897    }
898    ensure_non_empty(&matrix.profile_id, "federation_qualification.profile_id")?;
899    ensure_non_empty(
900        &matrix.exchange_ref,
901        "federation_qualification.exchange_ref",
902    )?;
903    ensure_non_empty(
904        &matrix.quorum_report_ref,
905        "federation_qualification.quorum_report_ref",
906    )?;
907    ensure_non_empty(
908        &matrix.reputation_clearing_ref,
909        "federation_qualification.reputation_clearing_ref",
910    )?;
911    if matrix.cases.is_empty() {
912        return Err(FederationContractError::MissingField(
913            "federation_qualification.cases",
914        ));
915    }
916
917    let mut case_ids = HashSet::new();
918    let mut covered_requirements = HashSet::new();
919    for case in &matrix.cases {
920        ensure_non_empty(&case.id, "federation_qualification.cases.id")?;
921        ensure_non_empty(&case.name, "federation_qualification.cases.name")?;
922        ensure_non_empty(&case.notes, "federation_qualification.cases.notes")?;
923        if case.requirement_ids.is_empty() {
924            return Err(FederationContractError::InvalidQualificationCase(format!(
925                "qualification case `{}` requires at least one requirement id",
926                case.id
927            )));
928        }
929        ensure_unique_strings(
930            &case.requirement_ids,
931            "federation_qualification.cases.requirement_ids",
932        )?;
933        if !case_ids.insert(case.id.as_str()) {
934            return Err(FederationContractError::DuplicateValue(case.id.clone()));
935        }
936        for requirement in &case.requirement_ids {
937            covered_requirements.insert(requirement.as_str());
938        }
939    }
940
941    for requirement in FEDERATION_REQUIRED_REQUIREMENTS {
942        if !covered_requirements.contains(requirement) {
943            return Err(FederationContractError::InvalidQualificationCase(format!(
944                "qualification matrix is missing coverage for {requirement}"
945            )));
946        }
947    }
948
949    Ok(())
950}
951
952fn validate_federation_artifact_reference(
953    reference: &FederationArtifactReference,
954    field: &'static str,
955) -> Result<(), FederationContractError> {
956    ensure_non_empty(&reference.schema, field)?;
957    ensure_non_empty(&reference.artifact_id, field)?;
958    ensure_non_empty(&reference.operator_id, field)?;
959    validate_hex_digest(&reference.sha256, field)
960}
961
962fn validate_federation_scope(scope: &FederationTrustScope) -> Result<(), FederationContractError> {
963    ensure_non_empty(&scope.namespace, "federation_scope.namespace")?;
964    ensure_non_empty(
965        &scope.subject_operator_id,
966        "federation_scope.subject_operator_id",
967    )?;
968    if scope.allowed_actor_kinds.is_empty() {
969        return Err(FederationContractError::MissingField(
970            "federation_scope.allowed_actor_kinds",
971        ));
972    }
973    if scope.allowed_admission_classes.is_empty() {
974        return Err(FederationContractError::MissingField(
975            "federation_scope.allowed_admission_classes",
976        ));
977    }
978    ensure_unique_copy_values(
979        &scope.allowed_actor_kinds,
980        "federation_scope.allowed_actor_kinds",
981    )?;
982    ensure_unique_copy_values(
983        &scope.allowed_admission_classes,
984        "federation_scope.allowed_admission_classes",
985    )?;
986    if let Some(policy_reference) = scope.policy_reference.as_deref() {
987        ensure_non_empty(policy_reference, "federation_scope.policy_reference")?;
988    }
989    Ok(())
990}
991
992fn validate_delegation_control(
993    control: &FederationDelegationControl,
994) -> Result<(), FederationContractError> {
995    ensure_non_empty(
996        &control.delegator_operator_id,
997        "federation_delegation.delegator_operator_id",
998    )?;
999    ensure_non_empty(
1000        &control.delegate_operator_id,
1001        "federation_delegation.delegate_operator_id",
1002    )?;
1003    if control.delegator_operator_id == control.delegate_operator_id {
1004        return Err(FederationContractError::InvalidExchange(
1005            "delegator and delegate operators must differ".to_string(),
1006        ));
1007    }
1008    if control.max_hops == 0 {
1009        return Err(FederationContractError::InvalidExchange(
1010            "delegation max_hops must be non-zero".to_string(),
1011        ));
1012    }
1013    if !control.attenuation_required {
1014        return Err(FederationContractError::InvalidExchange(
1015            "federation delegation must require attenuation".to_string(),
1016        ));
1017    }
1018    if !control.visibility_only_until_local_activation {
1019        return Err(FederationContractError::InvalidExchange(
1020            "federation delegation must remain visibility-only until local activation".to_string(),
1021        ));
1022    }
1023    Ok(())
1024}
1025
1026fn validate_import_control(
1027    control: &FederationImportControl,
1028) -> Result<(), FederationContractError> {
1029    if !control.explicit_local_activation_required {
1030        return Err(FederationContractError::InvalidExchange(
1031            "federation imports must require explicit local activation".to_string(),
1032        ));
1033    }
1034    if !control.manual_review_required {
1035        return Err(FederationContractError::InvalidExchange(
1036            "federation imports must require manual review".to_string(),
1037        ));
1038    }
1039    if !control.reject_stale_inputs {
1040        return Err(FederationContractError::InvalidExchange(
1041            "federation imports must reject stale inputs".to_string(),
1042        ));
1043    }
1044    if !control.allow_visibility_without_runtime_trust {
1045        return Err(FederationContractError::InvalidExchange(
1046            "federation imports must preserve visibility without runtime trust".to_string(),
1047        ));
1048    }
1049    if !control.prohibit_ambient_runtime_admission {
1050        return Err(FederationContractError::InvalidExchange(
1051            "federation imports must prohibit ambient runtime admission".to_string(),
1052        ));
1053    }
1054    Ok(())
1055}
1056
1057fn validate_anti_eclipse_policy(
1058    policy: &FederationAntiEclipsePolicy,
1059) -> Result<(), FederationContractError> {
1060    if policy.minimum_distinct_operators == 0 {
1061        return Err(FederationContractError::InvalidQuorum(
1062            "minimum_distinct_operators must be non-zero".to_string(),
1063        ));
1064    }
1065    Ok(())
1066}
1067
1068fn validate_stake_requirement(
1069    requirement: &FederatedStakeRequirement,
1070) -> Result<(), FederationContractError> {
1071    match requirement.admission_class {
1072        GenericTrustAdmissionClass::PublicUntrusted | GenericTrustAdmissionClass::Reviewable => {
1073            if requirement.required_bond_class.is_some()
1074                || requirement.minimum_bond_amount.is_some()
1075            {
1076                return Err(FederationContractError::InvalidAdmission(
1077                    "public_untrusted and reviewable admission cannot require bond collateral"
1078                        .to_string(),
1079                ));
1080            }
1081        }
1082        GenericTrustAdmissionClass::BondBacked => {
1083            let amount = requirement.minimum_bond_amount.as_ref().ok_or_else(|| {
1084                FederationContractError::InvalidAdmission(
1085                    "bond_backed admission requires minimum_bond_amount".to_string(),
1086                )
1087            })?;
1088            if requirement.required_bond_class.is_none() {
1089                return Err(FederationContractError::InvalidAdmission(
1090                    "bond_backed admission requires required_bond_class".to_string(),
1091                ));
1092            }
1093            if !requirement.slashable {
1094                return Err(FederationContractError::InvalidAdmission(
1095                    "bond_backed admission requires slashable collateral".to_string(),
1096                ));
1097            }
1098            validate_positive_money(amount, "federated_admission.minimum_bond_amount")?;
1099        }
1100        GenericTrustAdmissionClass::RoleGated => {
1101            if !requirement.governance_case_required {
1102                return Err(FederationContractError::InvalidAdmission(
1103                    "role_gated admission requires governance_case_required".to_string(),
1104                ));
1105            }
1106            if requirement.required_bond_class.is_some()
1107                || requirement.minimum_bond_amount.is_some()
1108            {
1109                return Err(FederationContractError::InvalidAdmission(
1110                    "role_gated admission should not infer a bond requirement".to_string(),
1111                ));
1112            }
1113        }
1114    }
1115    Ok(())
1116}
1117
1118fn validate_reputation_input_reference(
1119    input: &FederatedReputationInputReference,
1120    generated_at: u64,
1121    subject_key: &str,
1122) -> Result<(), FederationContractError> {
1123    validate_federation_artifact_reference(&input.artifact_ref, "federated_clearing.inputs")?;
1124    ensure_non_empty(&input.subject_key, "federated_clearing.inputs.subject_key")?;
1125    ensure_non_empty(
1126        &input.issuer_operator_id,
1127        "federated_clearing.inputs.issuer_operator_id",
1128    )?;
1129    if let Some(group_id) = input.issuer_independence_group_id.as_deref() {
1130        ensure_non_empty(
1131            group_id,
1132            "federated_clearing.inputs.issuer_independence_group_id",
1133        )?;
1134    }
1135    if input.subject_key != subject_key {
1136        return Err(FederationContractError::InvalidClearing(
1137            "reputation clearing inputs must target the same subject_key".to_string(),
1138        ));
1139    }
1140    if input.weight_bps == 0 || input.weight_bps > 10_000 {
1141        return Err(FederationContractError::InvalidClearing(
1142            "reputation clearing weight_bps must be between 1 and 10000".to_string(),
1143        ));
1144    }
1145    if input.published_at > generated_at {
1146        return Err(FederationContractError::InvalidClearing(
1147            "reputation clearing inputs cannot be published in the future".to_string(),
1148        ));
1149    }
1150    if let Some(expires_at) = input.expires_at {
1151        if expires_at <= input.published_at {
1152            return Err(FederationContractError::InvalidClearing(
1153                "reputation clearing input expires_at must be greater than published_at"
1154                    .to_string(),
1155            ));
1156        }
1157        if expires_at <= generated_at {
1158            return Err(FederationContractError::InvalidClearing(
1159                "reputation clearing inputs cannot be expired at generated_at".to_string(),
1160            ));
1161        }
1162    }
1163    match input.kind {
1164        FederatedReputationInputKind::ReputationSummary => {
1165            if input.blocking {
1166                return Err(FederationContractError::InvalidClearing(
1167                    "reputation summaries cannot be marked blocking".to_string(),
1168                ));
1169            }
1170            if input.artifact_ref.kind != FederationArtifactKind::PortableReputationSummary {
1171                return Err(FederationContractError::InvalidClearing(
1172                    "reputation summary inputs must reference portable reputation summaries"
1173                        .to_string(),
1174                ));
1175            }
1176        }
1177        FederatedReputationInputKind::NegativeEvent => {
1178            if input.artifact_ref.kind != FederationArtifactKind::PortableNegativeEvent {
1179                return Err(FederationContractError::InvalidClearing(
1180                    "negative event inputs must reference portable negative events".to_string(),
1181                ));
1182            }
1183        }
1184    }
1185    Ok(())
1186}
1187
1188fn validate_reputation_clearing_continuity(
1189    continuity: &FederatedReputationClearingContinuity,
1190    clearing_id: &str,
1191) -> Result<(), FederationContractError> {
1192    ensure_non_empty(
1193        &continuity.continuity_id,
1194        "federated_clearing.continuity.continuity_id",
1195    )?;
1196    if let Some(previous_clearing_id) = continuity.previous_clearing_id.as_deref() {
1197        ensure_non_empty(
1198            previous_clearing_id,
1199            "federated_clearing.continuity.previous_clearing_id",
1200        )?;
1201        if previous_clearing_id == clearing_id {
1202            return Err(FederationContractError::InvalidClearing(
1203                "continuity previous_clearing_id must differ from clearing_id".to_string(),
1204            ));
1205        }
1206    }
1207    Ok(())
1208}
1209
1210fn validate_sybil_control(control: &FederatedSybilControl) -> Result<(), FederationContractError> {
1211    if control.minimum_independent_issuers == 0 {
1212        return Err(FederationContractError::InvalidClearing(
1213            "minimum_independent_issuers must be non-zero".to_string(),
1214        ));
1215    }
1216    if control.maximum_inputs_per_issuer == 0 {
1217        return Err(FederationContractError::InvalidClearing(
1218            "maximum_inputs_per_issuer must be non-zero".to_string(),
1219        ));
1220    }
1221    if control.oracle_cap_bps == 0 || control.oracle_cap_bps > 10_000 {
1222        return Err(FederationContractError::InvalidClearing(
1223            "oracle_cap_bps must be between 1 and 10000".to_string(),
1224        ));
1225    }
1226    if !control.local_weighting_required {
1227        return Err(FederationContractError::InvalidClearing(
1228            "federated reputation clearing must require local weighting".to_string(),
1229        ));
1230    }
1231    Ok(())
1232}
1233
1234fn validate_positive_money(
1235    amount: &MonetaryAmount,
1236    field: &'static str,
1237) -> Result<(), FederationContractError> {
1238    if amount.units == 0 {
1239        return Err(FederationContractError::InvalidAdmission(format!(
1240            "{field} must be greater than zero"
1241        )));
1242    }
1243    let normalized = amount.currency.trim().to_ascii_uppercase();
1244    if normalized.len() != 3
1245        || !normalized
1246            .chars()
1247            .all(|character| character.is_ascii_uppercase())
1248    {
1249        return Err(FederationContractError::InvalidAdmission(format!(
1250            "{field} currency must be a 3-letter uppercase currency code"
1251        )));
1252    }
1253    Ok(())
1254}
1255
1256fn validate_hex_digest(value: &str, field: &'static str) -> Result<(), FederationContractError> {
1257    ensure_non_empty(value, field)?;
1258    let trimmed = value.trim();
1259    if trimmed.len() != 64
1260        || !trimmed
1261            .chars()
1262            .all(|character| character.is_ascii_hexdigit())
1263    {
1264        return Err(FederationContractError::InvalidReference(format!(
1265            "{field} must be a 64-character lowercase-compatible hex digest"
1266        )));
1267    }
1268    Ok(())
1269}
1270
1271fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), FederationContractError> {
1272    if value.trim().is_empty() {
1273        return Err(FederationContractError::MissingField(field));
1274    }
1275    Ok(())
1276}
1277
1278fn ensure_unique_strings(
1279    values: &[String],
1280    field: &'static str,
1281) -> Result<(), FederationContractError> {
1282    let mut seen = HashSet::new();
1283    for value in values {
1284        if value.trim().is_empty() {
1285            return Err(FederationContractError::MissingField(field));
1286        }
1287        if !seen.insert(value.as_str()) {
1288            return Err(FederationContractError::DuplicateValue(format!(
1289                "{field}:{value}"
1290            )));
1291        }
1292    }
1293    Ok(())
1294}
1295
1296fn ensure_unique_copy_values<T>(
1297    values: &[T],
1298    field: &'static str,
1299) -> Result<(), FederationContractError>
1300where
1301    T: Copy + Eq + std::hash::Hash + std::fmt::Debug,
1302{
1303    let mut seen = HashSet::new();
1304    for value in values {
1305        if !seen.insert(*value) {
1306            return Err(FederationContractError::DuplicateValue(format!(
1307                "{field}:{value:?}"
1308            )));
1309        }
1310    }
1311    Ok(())
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::*;
1317
1318    fn hex(seed: char) -> String {
1319        std::iter::repeat_n(seed, 64).collect()
1320    }
1321
1322    fn sample_reference(
1323        kind: FederationArtifactKind,
1324        schema: &str,
1325        artifact_id: &str,
1326        operator_id: &str,
1327        seed: char,
1328    ) -> FederationArtifactReference {
1329        FederationArtifactReference {
1330            kind,
1331            schema: schema.to_string(),
1332            artifact_id: artifact_id.to_string(),
1333            operator_id: operator_id.to_string(),
1334            sha256: hex(seed),
1335            uri: Some(format!(
1336                "https://{operator_id}.chio.example/artifacts/{artifact_id}"
1337            )),
1338        }
1339    }
1340
1341    fn sample_activation_exchange() -> FederationActivationExchangeArtifact {
1342        FederationActivationExchangeArtifact {
1343            schema: CHIO_FEDERATION_ACTIVATION_EXCHANGE_SCHEMA.to_string(),
1344            exchange_id: "fex-1".to_string(),
1345            issued_at: 1_743_552_000,
1346            expires_at: 1_743_638_400,
1347            source_operator_id: "origin-operator".to_string(),
1348            target_operator_id: "consumer-operator".to_string(),
1349            listing_id: "listing-liability-provider-1".to_string(),
1350            activation_ref: sample_reference(
1351                FederationArtifactKind::TrustActivation,
1352                "chio.registry.trust-activation.v1",
1353                "activation-1",
1354                "origin-operator",
1355                'a',
1356            ),
1357            listing_ref: sample_reference(
1358                FederationArtifactKind::Listing,
1359                "chio.registry.listing.v1",
1360                "listing-liability-provider-1",
1361                "origin-operator",
1362                'b',
1363            ),
1364            governing_charter_ref: Some(sample_reference(
1365                FederationArtifactKind::GovernanceCharter,
1366                "chio.registry.governance-charter.v1",
1367                "charter-1",
1368                "origin-operator",
1369                'c',
1370            )),
1371            scope: FederationTrustScope {
1372                namespace: "registry.chio.example/liability".to_string(),
1373                subject_operator_id: "origin-operator".to_string(),
1374                allowed_actor_kinds: vec![GenericListingActorKind::LiabilityProvider],
1375                allowed_admission_classes: vec![
1376                    GenericTrustAdmissionClass::Reviewable,
1377                    GenericTrustAdmissionClass::BondBacked,
1378                ],
1379                policy_reference: Some("policy/federation/default".to_string()),
1380            },
1381            delegation_control: FederationDelegationControl {
1382                delegator_operator_id: "origin-operator".to_string(),
1383                delegate_operator_id: "consumer-operator".to_string(),
1384                max_hops: 2,
1385                attenuation_required: true,
1386                visibility_only_until_local_activation: true,
1387            },
1388            import_control: FederationImportControl::default(),
1389            note: Some(
1390                "Shares one reviewed trust activation without widening runtime trust.".to_string(),
1391            ),
1392        }
1393    }
1394
1395    fn sample_quorum_report() -> FederationQuorumReport {
1396        FederationQuorumReport {
1397            schema: CHIO_FEDERATION_QUORUM_REPORT_SCHEMA.to_string(),
1398            report_id: "fqr-1".to_string(),
1399            generated_at: 1_743_552_060,
1400            namespace: "registry.chio.example/liability".to_string(),
1401            listing_id: "listing-liability-provider-1".to_string(),
1402            origin_operator_id: "origin-operator".to_string(),
1403            quorum_threshold: 2,
1404            max_replica_age_secs: 300,
1405            publishers: vec![
1406                FederationPublisherObservation {
1407                    publisher: GenericRegistryPublisher {
1408                        role: GenericRegistryPublisherRole::Origin,
1409                        operator_id: "origin-operator".to_string(),
1410                        operator_name: Some("Origin Operator".to_string()),
1411                        registry_url: "https://origin.chio.example/registry".to_string(),
1412                        upstream_registry_urls: vec![],
1413                    },
1414                    report_ref: sample_reference(
1415                        FederationArtifactKind::ListingReport,
1416                        "chio.registry.listing-report.v1",
1417                        "report-origin-1",
1418                        "origin-operator",
1419                        'd',
1420                    ),
1421                    observed_listing_sha256: hex('1'),
1422                    freshness: GenericListingReplicaFreshness {
1423                        state: GenericListingFreshnessState::Fresh,
1424                        age_secs: 30,
1425                        max_age_secs: 300,
1426                        valid_until: 1_743_552_360,
1427                        generated_at: 1_743_552_030,
1428                    },
1429                    observed_at: 1_743_552_030,
1430                    upstream_hop_count: 0,
1431                },
1432                FederationPublisherObservation {
1433                    publisher: GenericRegistryPublisher {
1434                        role: GenericRegistryPublisherRole::Mirror,
1435                        operator_id: "mirror-operator-a".to_string(),
1436                        operator_name: Some("Mirror Operator A".to_string()),
1437                        registry_url: "https://mirror-a.chio.example/registry".to_string(),
1438                        upstream_registry_urls: vec![
1439                            "https://origin.chio.example/registry".to_string(),
1440                        ],
1441                    },
1442                    report_ref: sample_reference(
1443                        FederationArtifactKind::ListingReport,
1444                        "chio.registry.listing-report.v1",
1445                        "report-mirror-a-1",
1446                        "mirror-operator-a",
1447                        'e',
1448                    ),
1449                    observed_listing_sha256: hex('1'),
1450                    freshness: GenericListingReplicaFreshness {
1451                        state: GenericListingFreshnessState::Fresh,
1452                        age_secs: 40,
1453                        max_age_secs: 300,
1454                        valid_until: 1_743_552_360,
1455                        generated_at: 1_743_552_020,
1456                    },
1457                    observed_at: 1_743_552_020,
1458                    upstream_hop_count: 1,
1459                },
1460                FederationPublisherObservation {
1461                    publisher: GenericRegistryPublisher {
1462                        role: GenericRegistryPublisherRole::Indexer,
1463                        operator_id: "indexer-operator-a".to_string(),
1464                        operator_name: Some("Indexer Operator A".to_string()),
1465                        registry_url: "https://indexer-a.chio.example/registry".to_string(),
1466                        upstream_registry_urls: vec![
1467                            "https://origin.chio.example/registry".to_string(),
1468                        ],
1469                    },
1470                    report_ref: sample_reference(
1471                        FederationArtifactKind::ListingReport,
1472                        "chio.registry.listing-report.v1",
1473                        "report-indexer-a-1",
1474                        "indexer-operator-a",
1475                        'f',
1476                    ),
1477                    observed_listing_sha256: hex('1'),
1478                    freshness: GenericListingReplicaFreshness {
1479                        state: GenericListingFreshnessState::Fresh,
1480                        age_secs: 45,
1481                        max_age_secs: 300,
1482                        valid_until: 1_743_552_360,
1483                        generated_at: 1_743_552_015,
1484                    },
1485                    observed_at: 1_743_552_015,
1486                    upstream_hop_count: 1,
1487                },
1488            ],
1489            conflicts: vec![],
1490            anti_eclipse_policy: FederationAntiEclipsePolicy::default(),
1491            final_state: FederationQuorumState::Converged,
1492            note: Some("Requires origin plus independent mirror/indexer observation before a remote listing is treated as converged."
1493                .to_string()),
1494        }
1495    }
1496
1497    fn sample_open_admission_policy() -> FederatedOpenAdmissionPolicyArtifact {
1498        FederatedOpenAdmissionPolicyArtifact {
1499            schema: CHIO_FEDERATION_OPEN_ADMISSION_POLICY_SCHEMA.to_string(),
1500            policy_id: "foap-1".to_string(),
1501            issued_at: 1_743_552_120,
1502            namespace: "registry.chio.example/liability".to_string(),
1503            governing_operator_id: "origin-operator".to_string(),
1504            allowed_admission_classes: vec![
1505                GenericTrustAdmissionClass::PublicUntrusted,
1506                GenericTrustAdmissionClass::Reviewable,
1507                GenericTrustAdmissionClass::BondBacked,
1508            ],
1509            stake_requirements: vec![FederatedStakeRequirement {
1510                admission_class: GenericTrustAdmissionClass::BondBacked,
1511                required_bond_class: Some(OpenMarketBondClass::Listing),
1512                minimum_bond_amount: Some(MonetaryAmount {
1513                    units: 10_000,
1514                    currency: "USD".to_string(),
1515                }),
1516                slashable: true,
1517                governance_case_required: false,
1518            }],
1519            governing_charter_ref: sample_reference(
1520                FederationArtifactKind::GovernanceCharter,
1521                "chio.registry.governance-charter.v1",
1522                "charter-1",
1523                "origin-operator",
1524                '2',
1525            ),
1526            fee_schedule_ref: sample_reference(
1527                FederationArtifactKind::OpenMarketFeeSchedule,
1528                "chio.registry.market-fee-schedule.v1",
1529                "fee-schedule-1",
1530                "origin-operator",
1531                '3',
1532            ),
1533            explicit_local_review_required: true,
1534            visibility_only_without_activation: true,
1535            note: Some("Allows public visibility, but runtime trust still requires explicit local review or bond-backed admission."
1536                .to_string()),
1537        }
1538    }
1539
1540    fn sample_reputation_clearing() -> FederatedReputationClearingArtifact {
1541        FederatedReputationClearingArtifact {
1542            schema: CHIO_FEDERATION_REPUTATION_CLEARING_SCHEMA.to_string(),
1543            clearing_id: "frc-1".to_string(),
1544            generated_at: 1_743_552_180,
1545            subject_key: "subject-1".to_string(),
1546            namespace: "registry.chio.example/liability".to_string(),
1547            participating_operator_ids: vec![
1548                "origin-operator".to_string(),
1549                "mirror-operator-a".to_string(),
1550                "indexer-operator-a".to_string(),
1551                "consumer-operator".to_string(),
1552            ],
1553            local_weighting_policy_ref: "policy/reputation/federated-default".to_string(),
1554            admission_policy_ref: "foap-1".to_string(),
1555            inputs: vec![
1556                FederatedReputationInputReference {
1557                    kind: FederatedReputationInputKind::ReputationSummary,
1558                    artifact_ref: sample_reference(
1559                        FederationArtifactKind::PortableReputationSummary,
1560                        "chio.portable-reputation-summary.v1",
1561                        "summary-origin-1",
1562                        "origin-operator",
1563                        '4',
1564                    ),
1565                    subject_key: "subject-1".to_string(),
1566                    issuer_operator_id: "origin-operator".to_string(),
1567                    issuer_independence_group_id: Some("operator-group-origin".to_string()),
1568                    weight_bps: 3_000,
1569                    blocking: false,
1570                    published_at: 1_743_552_000,
1571                    expires_at: Some(1_743_638_400),
1572                    note: Some("Origin-issued portable reputation summary.".to_string()),
1573                },
1574                FederatedReputationInputReference {
1575                    kind: FederatedReputationInputKind::ReputationSummary,
1576                    artifact_ref: sample_reference(
1577                        FederationArtifactKind::PortableReputationSummary,
1578                        "chio.portable-reputation-summary.v1",
1579                        "summary-mirror-a-1",
1580                        "mirror-operator-a",
1581                        '5',
1582                    ),
1583                    subject_key: "subject-1".to_string(),
1584                    issuer_operator_id: "mirror-operator-a".to_string(),
1585                    issuer_independence_group_id: Some("operator-group-mirror".to_string()),
1586                    weight_bps: 2_500,
1587                    blocking: false,
1588                    published_at: 1_743_552_010,
1589                    expires_at: Some(1_743_638_400),
1590                    note: Some("Mirror-issued portable reputation summary.".to_string()),
1591                },
1592                FederatedReputationInputReference {
1593                    kind: FederatedReputationInputKind::NegativeEvent,
1594                    artifact_ref: sample_reference(
1595                        FederationArtifactKind::PortableNegativeEvent,
1596                        "chio.portable-negative-event.v1",
1597                        "negative-indexer-a-1",
1598                        "indexer-operator-a",
1599                        '6',
1600                    ),
1601                    subject_key: "subject-1".to_string(),
1602                    issuer_operator_id: "indexer-operator-a".to_string(),
1603                    issuer_independence_group_id: Some("operator-group-indexer".to_string()),
1604                    weight_bps: 2_000,
1605                    blocking: true,
1606                    published_at: 1_743_552_020,
1607                    expires_at: Some(1_743_595_200),
1608                    note: Some("Indexers contribute corroborated negative-event evidence."
1609                        .to_string()),
1610                },
1611                FederatedReputationInputReference {
1612                    kind: FederatedReputationInputKind::NegativeEvent,
1613                    artifact_ref: sample_reference(
1614                        FederationArtifactKind::PortableNegativeEvent,
1615                        "chio.portable-negative-event.v1",
1616                        "negative-origin-1",
1617                        "origin-operator",
1618                        '7',
1619                    ),
1620                    subject_key: "subject-1".to_string(),
1621                    issuer_operator_id: "origin-operator".to_string(),
1622                    issuer_independence_group_id: Some("operator-group-origin".to_string()),
1623                    weight_bps: 1_500,
1624                    blocking: true,
1625                    published_at: 1_743_552_025,
1626                    expires_at: Some(1_743_595_200),
1627                    note: Some("Independent corroboration keeps a single issuer from becoming a universal oracle."
1628                        .to_string()),
1629                },
1630            ],
1631            sybil_control: FederatedSybilControl::default(),
1632            accepted_input_ids: vec![
1633                "summary-origin-1".to_string(),
1634                "summary-mirror-a-1".to_string(),
1635                "negative-indexer-a-1".to_string(),
1636                "negative-origin-1".to_string(),
1637            ],
1638            rejected_input_ids: vec![],
1639            effective_admission_class: GenericTrustAdmissionClass::Reviewable,
1640            continuity: Some(FederatedReputationClearingContinuity {
1641                continuity_id: "registry.chio.example/liability:subject-1".to_string(),
1642                previous_clearing_id: None,
1643            }),
1644            note: Some("Shared reputation clearing preserves local weighting and requires corroborated negative-event inputs."
1645                .to_string()),
1646        }
1647    }
1648
1649    fn sample_qualification_matrix() -> FederationQualificationMatrix {
1650        FederationQualificationMatrix {
1651            schema: CHIO_FEDERATION_QUALIFICATION_MATRIX_SCHEMA.to_string(),
1652            profile_id: "chio.federation.profile".to_string(),
1653            exchange_ref: "fex-1".to_string(),
1654            quorum_report_ref: "fqr-1".to_string(),
1655            reputation_clearing_ref: "frc-1".to_string(),
1656            cases: vec![
1657                FederationQualificationCase {
1658                    id: "activation-exchange".to_string(),
1659                    name: "Federated activation exchange stays visibility-first and locally reviewable"
1660                        .to_string(),
1661                    requirement_ids: vec!["TRUSTMAX-01".to_string()],
1662                    scenario: FederationScenarioKind::ConflictingActivation,
1663                    expected_outcome: FederationQualificationOutcome::Pass,
1664                    observed_outcome: FederationQualificationOutcome::Pass,
1665                    notes: "Remote trust activation remains an explicit exchange contract and never becomes ambient runtime trust."
1666                        .to_string(),
1667                },
1668                FederationQualificationCase {
1669                    id: "quorum-conflict".to_string(),
1670                    name: "Quorum, freshness, and anti-eclipse posture remain machine-reviewable"
1671                        .to_string(),
1672                    requirement_ids: vec!["TRUSTMAX-02".to_string()],
1673                    scenario: FederationScenarioKind::InsufficientQuorum,
1674                    expected_outcome: FederationQualificationOutcome::Pass,
1675                    observed_outcome: FederationQualificationOutcome::Pass,
1676                    notes: "Conflicting or stale publisher state fails closed instead of silently rewriting trust."
1677                        .to_string(),
1678                },
1679                FederationQualificationCase {
1680                    id: "open-admission-boundary".to_string(),
1681                    name: "Open admission stays bounded by explicit stake and review policy"
1682                        .to_string(),
1683                    requirement_ids: vec!["TRUSTMAX-03".to_string()],
1684                    scenario: FederationScenarioKind::GovernanceInterop,
1685                    expected_outcome: FederationQualificationOutcome::Pass,
1686                    observed_outcome: FederationQualificationOutcome::Pass,
1687                    notes: "Visibility and participation stay distinct from runtime trust, even when bond-backed admission is allowed."
1688                        .to_string(),
1689                },
1690                FederationQualificationCase {
1691                    id: "shared-reputation-sybil".to_string(),
1692                    name: "Shared reputation clearing resists duplicate-issuer and oracle collapse"
1693                        .to_string(),
1694                    requirement_ids: vec!["TRUSTMAX-04".to_string()],
1695                    scenario: FederationScenarioKind::ReputationSybil,
1696                    expected_outcome: FederationQualificationOutcome::Pass,
1697                    observed_outcome: FederationQualificationOutcome::Pass,
1698                    notes: "Accepted summaries come from distinct issuers and blocking negative events require corroboration."
1699                        .to_string(),
1700                },
1701                FederationQualificationCase {
1702                    id: "adversarial-federation".to_string(),
1703                    name: "Hostile publisher and eclipse attempts fail closed under the federation boundary"
1704                        .to_string(),
1705                    requirement_ids: vec!["TRUSTMAX-05".to_string()],
1706                    scenario: FederationScenarioKind::EclipseAttempt,
1707                    expected_outcome: FederationQualificationOutcome::Pass,
1708                    observed_outcome: FederationQualificationOutcome::Pass,
1709                    notes: "Hostile federation inputs remain visible but do not collapse governance or admission into ambient trust."
1710                        .to_string(),
1711                },
1712            ],
1713        }
1714    }
1715
1716    fn sample_conflict() -> FederationConflictEvidence {
1717        FederationConflictEvidence {
1718            divergence_key: "listing-liability-provider-1:hash-mismatch".to_string(),
1719            publisher_operator_ids: vec![
1720                "origin-operator".to_string(),
1721                "mirror-operator-a".to_string(),
1722            ],
1723            reason: "origin and mirror observed different listing bodies".to_string(),
1724        }
1725    }
1726
1727    #[test]
1728    fn activation_exchange_requires_local_policy_import() {
1729        let mut exchange = sample_activation_exchange();
1730        exchange.import_control.explicit_local_activation_required = false;
1731        assert!(matches!(
1732            validate_federation_activation_exchange(&exchange),
1733            Err(FederationContractError::InvalidExchange(_))
1734        ));
1735    }
1736
1737    #[test]
1738    fn quorum_report_requires_origin_publisher() {
1739        let mut report = sample_quorum_report();
1740        report.publishers.remove(0);
1741        assert!(matches!(
1742            validate_federation_quorum_report(&report),
1743            Err(FederationContractError::InvalidQuorum(_))
1744        ));
1745    }
1746
1747    #[test]
1748    fn open_admission_policy_requires_bond_requirement() {
1749        let mut policy = sample_open_admission_policy();
1750        policy.stake_requirements.clear();
1751        assert!(matches!(
1752            validate_federated_open_admission_policy(&policy),
1753            Err(FederationContractError::InvalidAdmission(_))
1754        ));
1755    }
1756
1757    #[test]
1758    fn reputation_clearing_rejects_duplicate_summary_issuer() {
1759        let mut clearing = sample_reputation_clearing();
1760        clearing.inputs[1].issuer_operator_id = "origin-operator".to_string();
1761        assert!(matches!(
1762            validate_federated_reputation_clearing(&clearing),
1763            Err(FederationContractError::InvalidClearing(_))
1764        ));
1765    }
1766
1767    #[test]
1768    fn reference_artifacts_parse_and_validate() {
1769        let exchange: FederationActivationExchangeArtifact = serde_json::from_str(include_str!(
1770            "../../../docs/standards/CHIO_FEDERATION_ACTIVATION_EXCHANGE_EXAMPLE.json"
1771        ))
1772        .unwrap();
1773        let quorum: FederationQuorumReport = serde_json::from_str(include_str!(
1774            "../../../docs/standards/CHIO_FEDERATION_QUORUM_REPORT_EXAMPLE.json"
1775        ))
1776        .unwrap();
1777        let admission: FederatedOpenAdmissionPolicyArtifact = serde_json::from_str(include_str!(
1778            "../../../docs/standards/CHIO_FEDERATION_OPEN_ADMISSION_POLICY_EXAMPLE.json"
1779        ))
1780        .unwrap();
1781        let clearing: FederatedReputationClearingArtifact = serde_json::from_str(include_str!(
1782            "../../../docs/standards/CHIO_FEDERATION_REPUTATION_CLEARING_EXAMPLE.json"
1783        ))
1784        .unwrap();
1785        let matrix: FederationQualificationMatrix = serde_json::from_str(include_str!(
1786            "../../../docs/standards/CHIO_FEDERATION_QUALIFICATION_MATRIX.json"
1787        ))
1788        .unwrap();
1789
1790        validate_federation_activation_exchange(&exchange).unwrap();
1791        validate_federation_quorum_report(&quorum).unwrap();
1792        validate_federated_open_admission_policy(&admission).unwrap();
1793        validate_federated_reputation_clearing(&clearing).unwrap();
1794        validate_federation_qualification_matrix(&matrix).unwrap();
1795    }
1796
1797    #[test]
1798    fn qualification_matrix_requires_requirement_coverage() {
1799        let mut matrix = sample_qualification_matrix();
1800        matrix.cases.pop();
1801        assert!(matches!(
1802            validate_federation_qualification_matrix(&matrix),
1803            Err(FederationContractError::InvalidQualificationCase(_))
1804        ));
1805    }
1806
1807    #[test]
1808    fn activation_exchange_rejects_schema_reference_and_operator_mismatches() {
1809        let mut exchange = sample_activation_exchange();
1810        exchange.schema = "chio.federation-activation-exchange.v0".to_string();
1811        assert!(matches!(
1812            validate_federation_activation_exchange(&exchange),
1813            Err(FederationContractError::UnsupportedSchema(_))
1814        ));
1815
1816        let mut exchange = sample_activation_exchange();
1817        exchange.target_operator_id = exchange.source_operator_id.clone();
1818        assert!(matches!(
1819            validate_federation_activation_exchange(&exchange),
1820            Err(FederationContractError::InvalidExchange(_))
1821        ));
1822
1823        let mut exchange = sample_activation_exchange();
1824        exchange.expires_at = exchange.issued_at;
1825        assert!(matches!(
1826            validate_federation_activation_exchange(&exchange),
1827            Err(FederationContractError::InvalidExchange(_))
1828        ));
1829
1830        let mut exchange = sample_activation_exchange();
1831        exchange.activation_ref.kind = FederationArtifactKind::Listing;
1832        assert!(matches!(
1833            validate_federation_activation_exchange(&exchange),
1834            Err(FederationContractError::InvalidExchange(_))
1835        ));
1836
1837        let mut exchange = sample_activation_exchange();
1838        exchange.listing_ref.kind = FederationArtifactKind::TrustActivation;
1839        assert!(matches!(
1840            validate_federation_activation_exchange(&exchange),
1841            Err(FederationContractError::InvalidExchange(_))
1842        ));
1843
1844        let mut exchange = sample_activation_exchange();
1845        exchange
1846            .governing_charter_ref
1847            .as_mut()
1848            .expect("charter")
1849            .kind = FederationArtifactKind::Listing;
1850        assert!(matches!(
1851            validate_federation_activation_exchange(&exchange),
1852            Err(FederationContractError::InvalidExchange(_))
1853        ));
1854
1855        let mut exchange = sample_activation_exchange();
1856        exchange.delegation_control.delegator_operator_id = "other-operator".to_string();
1857        assert!(matches!(
1858            validate_federation_activation_exchange(&exchange),
1859            Err(FederationContractError::InvalidExchange(_))
1860        ));
1861
1862        let mut exchange = sample_activation_exchange();
1863        exchange.delegation_control.delegate_operator_id = "other-operator".to_string();
1864        assert!(matches!(
1865            validate_federation_activation_exchange(&exchange),
1866            Err(FederationContractError::InvalidExchange(_))
1867        ));
1868    }
1869
1870    #[test]
1871    fn federation_helper_validators_reject_invalid_boundary_inputs() {
1872        let mut reference = sample_reference(
1873            FederationArtifactKind::Listing,
1874            "chio.registry.listing.v1",
1875            "listing-1",
1876            "origin-operator",
1877            'a',
1878        );
1879        reference.sha256 = "deadbeef".to_string();
1880        assert!(matches!(
1881            validate_federation_artifact_reference(&reference, "reference"),
1882            Err(FederationContractError::InvalidReference(_))
1883        ));
1884
1885        let mut scope = sample_activation_exchange().scope;
1886        scope.allowed_actor_kinds.clear();
1887        assert!(matches!(
1888            validate_federation_scope(&scope),
1889            Err(FederationContractError::MissingField(_))
1890        ));
1891
1892        let mut scope = sample_activation_exchange().scope;
1893        scope.allowed_admission_classes.clear();
1894        assert!(matches!(
1895            validate_federation_scope(&scope),
1896            Err(FederationContractError::MissingField(_))
1897        ));
1898
1899        let mut scope = sample_activation_exchange().scope;
1900        scope.policy_reference = Some("   ".to_string());
1901        assert!(matches!(
1902            validate_federation_scope(&scope),
1903            Err(FederationContractError::MissingField(_))
1904        ));
1905
1906        let mut control = sample_activation_exchange().delegation_control;
1907        control.delegate_operator_id = control.delegator_operator_id.clone();
1908        assert!(matches!(
1909            validate_delegation_control(&control),
1910            Err(FederationContractError::InvalidExchange(_))
1911        ));
1912
1913        let mut control = sample_activation_exchange().delegation_control;
1914        control.max_hops = 0;
1915        assert!(matches!(
1916            validate_delegation_control(&control),
1917            Err(FederationContractError::InvalidExchange(_))
1918        ));
1919
1920        let mut control = sample_activation_exchange().delegation_control;
1921        control.attenuation_required = false;
1922        assert!(matches!(
1923            validate_delegation_control(&control),
1924            Err(FederationContractError::InvalidExchange(_))
1925        ));
1926
1927        let mut control = sample_activation_exchange().delegation_control;
1928        control.visibility_only_until_local_activation = false;
1929        assert!(matches!(
1930            validate_delegation_control(&control),
1931            Err(FederationContractError::InvalidExchange(_))
1932        ));
1933
1934        let mut import = FederationImportControl::default();
1935        import.manual_review_required = false;
1936        assert!(matches!(
1937            validate_import_control(&import),
1938            Err(FederationContractError::InvalidExchange(_))
1939        ));
1940
1941        let mut import = FederationImportControl::default();
1942        import.reject_stale_inputs = false;
1943        assert!(matches!(
1944            validate_import_control(&import),
1945            Err(FederationContractError::InvalidExchange(_))
1946        ));
1947
1948        let mut import = FederationImportControl::default();
1949        import.allow_visibility_without_runtime_trust = false;
1950        assert!(matches!(
1951            validate_import_control(&import),
1952            Err(FederationContractError::InvalidExchange(_))
1953        ));
1954
1955        let mut import = FederationImportControl::default();
1956        import.prohibit_ambient_runtime_admission = false;
1957        assert!(matches!(
1958            validate_import_control(&import),
1959            Err(FederationContractError::InvalidExchange(_))
1960        ));
1961
1962        let mut anti_eclipse = FederationAntiEclipsePolicy::default();
1963        anti_eclipse.minimum_distinct_operators = 0;
1964        assert!(matches!(
1965            validate_anti_eclipse_policy(&anti_eclipse),
1966            Err(FederationContractError::InvalidQuorum(_))
1967        ));
1968
1969        assert!(matches!(
1970            validate_positive_money(
1971                &MonetaryAmount {
1972                    units: 0,
1973                    currency: "USD".to_string(),
1974                },
1975                "bond"
1976            ),
1977            Err(FederationContractError::InvalidAdmission(_))
1978        ));
1979
1980        assert!(matches!(
1981            validate_positive_money(
1982                &MonetaryAmount {
1983                    units: 10,
1984                    currency: "US".to_string(),
1985                },
1986                "bond"
1987            ),
1988            Err(FederationContractError::InvalidAdmission(_))
1989        ));
1990
1991        assert!(matches!(
1992            validate_hex_digest("", "digest"),
1993            Err(FederationContractError::MissingField(_))
1994        ));
1995
1996        assert!(matches!(
1997            validate_hex_digest("xyz", "digest"),
1998            Err(FederationContractError::InvalidReference(_))
1999        ));
2000
2001        assert!(matches!(
2002            ensure_unique_strings(&["origin".to_string(), "origin".to_string()], "operators"),
2003            Err(FederationContractError::DuplicateValue(_))
2004        ));
2005
2006        assert!(matches!(
2007            ensure_unique_copy_values(
2008                &[
2009                    GenericTrustAdmissionClass::Reviewable,
2010                    GenericTrustAdmissionClass::Reviewable,
2011                ],
2012                "classes"
2013            ),
2014            Err(FederationContractError::DuplicateValue(_))
2015        ));
2016    }
2017
2018    #[test]
2019    fn quorum_report_rejects_invalid_observations_and_policy_failures() {
2020        let mut report = sample_quorum_report();
2021        report.schema = "chio.federation-quorum-report.v0".to_string();
2022        assert!(matches!(
2023            validate_federation_quorum_report(&report),
2024            Err(FederationContractError::UnsupportedSchema(_))
2025        ));
2026
2027        let mut report = sample_quorum_report();
2028        report.quorum_threshold = 0;
2029        assert!(matches!(
2030            validate_federation_quorum_report(&report),
2031            Err(FederationContractError::InvalidQuorum(_))
2032        ));
2033
2034        let mut report = sample_quorum_report();
2035        report.max_replica_age_secs = 0;
2036        assert!(matches!(
2037            validate_federation_quorum_report(&report),
2038            Err(FederationContractError::InvalidQuorum(_))
2039        ));
2040
2041        let mut report = sample_quorum_report();
2042        report.publishers.clear();
2043        assert!(matches!(
2044            validate_federation_quorum_report(&report),
2045            Err(FederationContractError::MissingField(_))
2046        ));
2047
2048        let mut report = sample_quorum_report();
2049        report.publishers[0].report_ref.kind = FederationArtifactKind::Listing;
2050        assert!(matches!(
2051            validate_federation_quorum_report(&report),
2052            Err(FederationContractError::InvalidQuorum(_))
2053        ));
2054
2055        let mut report = sample_quorum_report();
2056        report.publishers[0].report_ref.operator_id = "other-operator".to_string();
2057        assert!(matches!(
2058            validate_federation_quorum_report(&report),
2059            Err(FederationContractError::InvalidQuorum(_))
2060        ));
2061
2062        let mut report = sample_quorum_report();
2063        report.publishers[0].observed_listing_sha256 = "bad-digest".to_string();
2064        assert!(matches!(
2065            validate_federation_quorum_report(&report),
2066            Err(FederationContractError::InvalidReference(_))
2067        ));
2068
2069        let mut report = sample_quorum_report();
2070        report.publishers[1].upstream_hop_count = 2;
2071        assert!(matches!(
2072            validate_federation_quorum_report(&report),
2073            Err(FederationContractError::InvalidQuorum(_))
2074        ));
2075
2076        let mut report = sample_quorum_report();
2077        report.publishers[1].publisher.operator_id =
2078            report.publishers[0].publisher.operator_id.clone();
2079        report.publishers[1].report_ref.operator_id =
2080            report.publishers[0].publisher.operator_id.clone();
2081        assert!(matches!(
2082            validate_federation_quorum_report(&report),
2083            Err(FederationContractError::DuplicateValue(_))
2084        ));
2085
2086        let mut report = sample_quorum_report();
2087        report
2088            .publishers
2089            .retain(|publisher| publisher.publisher.role != GenericRegistryPublisherRole::Indexer);
2090        assert!(matches!(
2091            validate_federation_quorum_report(&report),
2092            Err(FederationContractError::InvalidQuorum(_))
2093        ));
2094
2095        let mut report = sample_quorum_report();
2096        report.anti_eclipse_policy.minimum_distinct_operators = 4;
2097        assert!(matches!(
2098            validate_federation_quorum_report(&report),
2099            Err(FederationContractError::InvalidQuorum(_))
2100        ));
2101    }
2102
2103    #[test]
2104    fn quorum_report_rejects_invalid_conflict_and_state_combinations() {
2105        let mut report = sample_quorum_report();
2106        report.conflicts = vec![sample_conflict(), sample_conflict()];
2107        report.final_state = FederationQuorumState::Conflicting;
2108        assert!(matches!(
2109            validate_federation_quorum_report(&report),
2110            Err(FederationContractError::DuplicateValue(_))
2111        ));
2112
2113        let mut report = sample_quorum_report();
2114        report.conflicts.push(sample_conflict());
2115        assert!(matches!(
2116            validate_federation_quorum_report(&report),
2117            Err(FederationContractError::InvalidQuorum(_))
2118        ));
2119
2120        let mut report = sample_quorum_report();
2121        report.publishers[0].freshness.state = GenericListingFreshnessState::Stale;
2122        report.publishers[0].freshness.age_secs = 400;
2123        report.publishers[1].freshness.state = GenericListingFreshnessState::Stale;
2124        report.publishers[1].freshness.age_secs = 400;
2125        assert!(matches!(
2126            validate_federation_quorum_report(&report),
2127            Err(FederationContractError::InvalidQuorum(_))
2128        ));
2129
2130        let mut report = sample_quorum_report();
2131        report.final_state = FederationQuorumState::Conflicting;
2132        assert!(matches!(
2133            validate_federation_quorum_report(&report),
2134            Err(FederationContractError::InvalidQuorum(_))
2135        ));
2136
2137        let mut report = sample_quorum_report();
2138        report.final_state = FederationQuorumState::InsufficientQuorum;
2139        assert!(matches!(
2140            validate_federation_quorum_report(&report),
2141            Err(FederationContractError::InvalidQuorum(_))
2142        ));
2143
2144        let mut report = sample_quorum_report();
2145        report.final_state = FederationQuorumState::Stale;
2146        report.publishers[0].freshness.state = GenericListingFreshnessState::Fresh;
2147        assert!(matches!(
2148            validate_federation_quorum_report(&report),
2149            Err(FederationContractError::InvalidQuorum(_))
2150        ));
2151    }
2152
2153    #[test]
2154    fn open_admission_policy_and_stake_rules_reject_invalid_configurations() {
2155        let mut policy = sample_open_admission_policy();
2156        policy.schema = "chio.federation-open-admission-policy.v0".to_string();
2157        assert!(matches!(
2158            validate_federated_open_admission_policy(&policy),
2159            Err(FederationContractError::UnsupportedSchema(_))
2160        ));
2161
2162        let mut policy = sample_open_admission_policy();
2163        policy.allowed_admission_classes.clear();
2164        assert!(matches!(
2165            validate_federated_open_admission_policy(&policy),
2166            Err(FederationContractError::MissingField(_))
2167        ));
2168
2169        let mut policy = sample_open_admission_policy();
2170        policy
2171            .allowed_admission_classes
2172            .push(GenericTrustAdmissionClass::Reviewable);
2173        assert!(matches!(
2174            validate_federated_open_admission_policy(&policy),
2175            Err(FederationContractError::DuplicateValue(_))
2176        ));
2177
2178        let mut policy = sample_open_admission_policy();
2179        policy.governing_charter_ref.kind = FederationArtifactKind::Listing;
2180        assert!(matches!(
2181            validate_federated_open_admission_policy(&policy),
2182            Err(FederationContractError::InvalidAdmission(_))
2183        ));
2184
2185        let mut policy = sample_open_admission_policy();
2186        policy.fee_schedule_ref.kind = FederationArtifactKind::GovernanceCharter;
2187        assert!(matches!(
2188            validate_federated_open_admission_policy(&policy),
2189            Err(FederationContractError::InvalidAdmission(_))
2190        ));
2191
2192        let mut policy = sample_open_admission_policy();
2193        policy.explicit_local_review_required = false;
2194        assert!(matches!(
2195            validate_federated_open_admission_policy(&policy),
2196            Err(FederationContractError::InvalidAdmission(_))
2197        ));
2198
2199        let mut policy = sample_open_admission_policy();
2200        policy.visibility_only_without_activation = false;
2201        assert!(matches!(
2202            validate_federated_open_admission_policy(&policy),
2203            Err(FederationContractError::InvalidAdmission(_))
2204        ));
2205
2206        let mut policy = sample_open_admission_policy();
2207        policy.stake_requirements[0].admission_class = GenericTrustAdmissionClass::RoleGated;
2208        assert!(matches!(
2209            validate_federated_open_admission_policy(&policy),
2210            Err(FederationContractError::InvalidAdmission(_))
2211        ));
2212
2213        let mut policy = sample_open_admission_policy();
2214        let duplicate = policy.stake_requirements[0].clone();
2215        policy.stake_requirements.push(duplicate);
2216        assert!(matches!(
2217            validate_federated_open_admission_policy(&policy),
2218            Err(FederationContractError::DuplicateValue(_))
2219        ));
2220
2221        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2222        requirement.minimum_bond_amount = None;
2223        assert!(matches!(
2224            validate_stake_requirement(&requirement),
2225            Err(FederationContractError::InvalidAdmission(_))
2226        ));
2227
2228        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2229        requirement.required_bond_class = None;
2230        assert!(matches!(
2231            validate_stake_requirement(&requirement),
2232            Err(FederationContractError::InvalidAdmission(_))
2233        ));
2234
2235        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2236        requirement.slashable = false;
2237        assert!(matches!(
2238            validate_stake_requirement(&requirement),
2239            Err(FederationContractError::InvalidAdmission(_))
2240        ));
2241
2242        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2243        requirement.minimum_bond_amount = Some(MonetaryAmount {
2244            units: 0,
2245            currency: "USD".to_string(),
2246        });
2247        assert!(matches!(
2248            validate_stake_requirement(&requirement),
2249            Err(FederationContractError::InvalidAdmission(_))
2250        ));
2251
2252        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2253        requirement.minimum_bond_amount = Some(MonetaryAmount {
2254            units: 10,
2255            currency: "US".to_string(),
2256        });
2257        assert!(matches!(
2258            validate_stake_requirement(&requirement),
2259            Err(FederationContractError::InvalidAdmission(_))
2260        ));
2261
2262        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2263        requirement.admission_class = GenericTrustAdmissionClass::PublicUntrusted;
2264        assert!(matches!(
2265            validate_stake_requirement(&requirement),
2266            Err(FederationContractError::InvalidAdmission(_))
2267        ));
2268
2269        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2270        requirement.admission_class = GenericTrustAdmissionClass::RoleGated;
2271        requirement.required_bond_class = None;
2272        requirement.minimum_bond_amount = None;
2273        requirement.governance_case_required = false;
2274        assert!(matches!(
2275            validate_stake_requirement(&requirement),
2276            Err(FederationContractError::InvalidAdmission(_))
2277        ));
2278
2279        let mut requirement = sample_open_admission_policy().stake_requirements[0].clone();
2280        requirement.admission_class = GenericTrustAdmissionClass::RoleGated;
2281        requirement.governance_case_required = true;
2282        assert!(matches!(
2283            validate_stake_requirement(&requirement),
2284            Err(FederationContractError::InvalidAdmission(_))
2285        ));
2286    }
2287
2288    #[test]
2289    fn reputation_input_and_sybil_helpers_reject_invalid_values() {
2290        let clearing = sample_reputation_clearing();
2291
2292        let mut input = clearing.inputs[0].clone();
2293        input.subject_key = "someone-else".to_string();
2294        assert!(matches!(
2295            validate_reputation_input_reference(
2296                &input,
2297                clearing.generated_at,
2298                &clearing.subject_key
2299            ),
2300            Err(FederationContractError::InvalidClearing(_))
2301        ));
2302
2303        let mut input = clearing.inputs[0].clone();
2304        input.weight_bps = 0;
2305        assert!(matches!(
2306            validate_reputation_input_reference(
2307                &input,
2308                clearing.generated_at,
2309                &clearing.subject_key
2310            ),
2311            Err(FederationContractError::InvalidClearing(_))
2312        ));
2313
2314        let mut input = clearing.inputs[0].clone();
2315        input.published_at = clearing.generated_at + 1;
2316        assert!(matches!(
2317            validate_reputation_input_reference(
2318                &input,
2319                clearing.generated_at,
2320                &clearing.subject_key
2321            ),
2322            Err(FederationContractError::InvalidClearing(_))
2323        ));
2324
2325        let mut input = clearing.inputs[0].clone();
2326        input.expires_at = Some(input.published_at);
2327        assert!(matches!(
2328            validate_reputation_input_reference(
2329                &input,
2330                clearing.generated_at,
2331                &clearing.subject_key
2332            ),
2333            Err(FederationContractError::InvalidClearing(_))
2334        ));
2335
2336        let mut input = clearing.inputs[0].clone();
2337        input.expires_at = Some(clearing.generated_at);
2338        assert!(matches!(
2339            validate_reputation_input_reference(
2340                &input,
2341                clearing.generated_at,
2342                &clearing.subject_key
2343            ),
2344            Err(FederationContractError::InvalidClearing(_))
2345        ));
2346
2347        let mut input = clearing.inputs[0].clone();
2348        input.issuer_independence_group_id = Some("   ".to_string());
2349        assert!(matches!(
2350            validate_reputation_input_reference(
2351                &input,
2352                clearing.generated_at,
2353                &clearing.subject_key
2354            ),
2355            Err(FederationContractError::MissingField(_))
2356        ));
2357
2358        let mut input = clearing.inputs[0].clone();
2359        input.blocking = true;
2360        assert!(matches!(
2361            validate_reputation_input_reference(
2362                &input,
2363                clearing.generated_at,
2364                &clearing.subject_key
2365            ),
2366            Err(FederationContractError::InvalidClearing(_))
2367        ));
2368
2369        let mut input = clearing.inputs[0].clone();
2370        input.artifact_ref.kind = FederationArtifactKind::PortableNegativeEvent;
2371        assert!(matches!(
2372            validate_reputation_input_reference(
2373                &input,
2374                clearing.generated_at,
2375                &clearing.subject_key
2376            ),
2377            Err(FederationContractError::InvalidClearing(_))
2378        ));
2379
2380        let mut input = clearing.inputs[2].clone();
2381        input.artifact_ref.kind = FederationArtifactKind::PortableReputationSummary;
2382        assert!(matches!(
2383            validate_reputation_input_reference(
2384                &input,
2385                clearing.generated_at,
2386                &clearing.subject_key
2387            ),
2388            Err(FederationContractError::InvalidClearing(_))
2389        ));
2390
2391        let mut sybil = FederatedSybilControl::default();
2392        sybil.minimum_independent_issuers = 0;
2393        assert!(matches!(
2394            validate_sybil_control(&sybil),
2395            Err(FederationContractError::InvalidClearing(_))
2396        ));
2397
2398        let mut sybil = FederatedSybilControl::default();
2399        sybil.maximum_inputs_per_issuer = 0;
2400        assert!(matches!(
2401            validate_sybil_control(&sybil),
2402            Err(FederationContractError::InvalidClearing(_))
2403        ));
2404
2405        let mut sybil = FederatedSybilControl::default();
2406        sybil.oracle_cap_bps = 10_001;
2407        assert!(matches!(
2408            validate_sybil_control(&sybil),
2409            Err(FederationContractError::InvalidClearing(_))
2410        ));
2411
2412        let mut sybil = FederatedSybilControl::default();
2413        sybil.local_weighting_required = false;
2414        assert!(matches!(
2415            validate_sybil_control(&sybil),
2416            Err(FederationContractError::InvalidClearing(_))
2417        ));
2418    }
2419
2420    #[test]
2421    fn reputation_clearing_rejects_invalid_classification_and_sybil_outcomes() {
2422        let mut clearing = sample_reputation_clearing();
2423        clearing.schema = "chio.federation-reputation-clearing.v0".to_string();
2424        assert!(matches!(
2425            validate_federated_reputation_clearing(&clearing),
2426            Err(FederationContractError::UnsupportedSchema(_))
2427        ));
2428
2429        let mut clearing = sample_reputation_clearing();
2430        clearing.participating_operator_ids.clear();
2431        assert!(matches!(
2432            validate_federated_reputation_clearing(&clearing),
2433            Err(FederationContractError::MissingField(_))
2434        ));
2435
2436        let mut clearing = sample_reputation_clearing();
2437        clearing.participating_operator_ids[1] = clearing.participating_operator_ids[0].clone();
2438        assert!(matches!(
2439            validate_federated_reputation_clearing(&clearing),
2440            Err(FederationContractError::DuplicateValue(_))
2441        ));
2442
2443        let mut clearing = sample_reputation_clearing();
2444        clearing.inputs.clear();
2445        assert!(matches!(
2446            validate_federated_reputation_clearing(&clearing),
2447            Err(FederationContractError::MissingField(_))
2448        ));
2449
2450        let mut clearing = sample_reputation_clearing();
2451        clearing
2452            .accepted_input_ids
2453            .push("summary-origin-1".to_string());
2454        assert!(matches!(
2455            validate_federated_reputation_clearing(&clearing),
2456            Err(FederationContractError::DuplicateValue(_))
2457        ));
2458
2459        let mut clearing = sample_reputation_clearing();
2460        clearing.rejected_input_ids = vec![
2461            "negative-origin-1".to_string(),
2462            "negative-origin-1".to_string(),
2463        ];
2464        clearing
2465            .accepted_input_ids
2466            .retain(|id| id != "negative-origin-1");
2467        assert!(matches!(
2468            validate_federated_reputation_clearing(&clearing),
2469            Err(FederationContractError::DuplicateValue(_))
2470        ));
2471
2472        let mut clearing = sample_reputation_clearing();
2473        clearing
2474            .rejected_input_ids
2475            .push("summary-origin-1".to_string());
2476        assert!(matches!(
2477            validate_federated_reputation_clearing(&clearing),
2478            Err(FederationContractError::InvalidClearing(_))
2479        ));
2480
2481        let mut clearing = sample_reputation_clearing();
2482        clearing.accepted_input_ids.pop();
2483        assert!(matches!(
2484            validate_federated_reputation_clearing(&clearing),
2485            Err(FederationContractError::InvalidClearing(_))
2486        ));
2487
2488        let mut clearing = sample_reputation_clearing();
2489        clearing.inputs[1].issuer_operator_id = "origin-operator".to_string();
2490        clearing.inputs[1].artifact_ref.operator_id = "origin-operator".to_string();
2491        assert!(matches!(
2492            validate_federated_reputation_clearing(&clearing),
2493            Err(FederationContractError::InvalidClearing(_))
2494        ));
2495
2496        let mut clearing = sample_reputation_clearing();
2497        clearing.inputs[0].artifact_ref.operator_id = "other-operator".to_string();
2498        assert!(matches!(
2499            validate_federated_reputation_clearing(&clearing),
2500            Err(FederationContractError::InvalidClearing(_))
2501        ));
2502
2503        let mut clearing = sample_reputation_clearing();
2504        clearing.inputs[0].issuer_operator_id = "other-operator".to_string();
2505        clearing.inputs[0].artifact_ref.operator_id = "other-operator".to_string();
2506        assert!(matches!(
2507            validate_federated_reputation_clearing(&clearing),
2508            Err(FederationContractError::InvalidClearing(_))
2509        ));
2510
2511        let mut clearing = sample_reputation_clearing();
2512        clearing.inputs[0].weight_bps = 4_001;
2513        assert!(matches!(
2514            validate_federated_reputation_clearing(&clearing),
2515            Err(FederationContractError::InvalidClearing(_))
2516        ));
2517
2518        let mut clearing = sample_reputation_clearing();
2519        clearing.accepted_input_ids = vec![
2520            "summary-origin-1".to_string(),
2521            "negative-origin-1".to_string(),
2522        ];
2523        clearing.rejected_input_ids = vec![
2524            "summary-mirror-a-1".to_string(),
2525            "negative-indexer-a-1".to_string(),
2526        ];
2527        assert!(matches!(
2528            validate_federated_reputation_clearing(&clearing),
2529            Err(FederationContractError::InvalidClearing(_))
2530        ));
2531
2532        let mut clearing = sample_reputation_clearing();
2533        clearing.accepted_input_ids = vec![
2534            "summary-origin-1".to_string(),
2535            "summary-mirror-a-1".to_string(),
2536            "negative-indexer-a-1".to_string(),
2537        ];
2538        clearing.rejected_input_ids = vec!["negative-origin-1".to_string()];
2539        assert!(matches!(
2540            validate_federated_reputation_clearing(&clearing),
2541            Err(FederationContractError::InvalidClearing(_))
2542        ));
2543
2544        let mut clearing = sample_reputation_clearing();
2545        clearing.sybil_control.minimum_independent_issuers = 3;
2546        clearing.inputs[1].issuer_independence_group_id =
2547            clearing.inputs[0].issuer_independence_group_id.clone();
2548        assert!(matches!(
2549            validate_federated_reputation_clearing(&clearing),
2550            Err(FederationContractError::InvalidClearing(_))
2551        ));
2552
2553        let mut clearing = sample_reputation_clearing();
2554        clearing.accepted_input_ids.clear();
2555        clearing.rejected_input_ids = clearing
2556            .inputs
2557            .iter()
2558            .map(|input| input.artifact_ref.artifact_id.clone())
2559            .collect();
2560        assert!(matches!(
2561            validate_federated_reputation_clearing(&clearing),
2562            Err(FederationContractError::InvalidClearing(_))
2563        ));
2564
2565        let mut clearing = sample_reputation_clearing();
2566        clearing.continuity.as_mut().unwrap().previous_clearing_id =
2567            Some(clearing.clearing_id.clone());
2568        assert!(matches!(
2569            validate_federated_reputation_clearing(&clearing),
2570            Err(FederationContractError::InvalidClearing(_))
2571        ));
2572    }
2573
2574    #[test]
2575    fn qualification_matrix_rejects_case_level_misconfigurations() {
2576        let mut matrix = sample_qualification_matrix();
2577        matrix.schema = "chio.federation-qualification-matrix.v0".to_string();
2578        assert!(matches!(
2579            validate_federation_qualification_matrix(&matrix),
2580            Err(FederationContractError::UnsupportedSchema(_))
2581        ));
2582
2583        let mut matrix = sample_qualification_matrix();
2584        matrix.cases.clear();
2585        assert!(matches!(
2586            validate_federation_qualification_matrix(&matrix),
2587            Err(FederationContractError::MissingField(_))
2588        ));
2589
2590        let mut matrix = sample_qualification_matrix();
2591        matrix.cases[0].requirement_ids.clear();
2592        assert!(matches!(
2593            validate_federation_qualification_matrix(&matrix),
2594            Err(FederationContractError::InvalidQualificationCase(_))
2595        ));
2596
2597        let mut matrix = sample_qualification_matrix();
2598        matrix.cases[0]
2599            .requirement_ids
2600            .push("TRUSTMAX-01".to_string());
2601        assert!(matches!(
2602            validate_federation_qualification_matrix(&matrix),
2603            Err(FederationContractError::DuplicateValue(_))
2604        ));
2605
2606        let mut matrix = sample_qualification_matrix();
2607        matrix.cases[1].id = matrix.cases[0].id.clone();
2608        assert!(matches!(
2609            validate_federation_qualification_matrix(&matrix),
2610            Err(FederationContractError::DuplicateValue(_))
2611        ));
2612
2613        let mut matrix = sample_qualification_matrix();
2614        matrix.cases[0].notes.clear();
2615        assert!(matches!(
2616            validate_federation_qualification_matrix(&matrix),
2617            Err(FederationContractError::MissingField(_))
2618        ));
2619    }
2620}