1pub 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}