Chio/Docs
LOGIN · JOIN

PlatformFederation

Federation & Compliance

Quorum & Anti-Eclipse

The read-path quorum over what other operators publish: one report, four policy checks that refuse it outright, and four final states a validator accepts.

Read path here, write path next door

Two things in Chio are called quorum and they never touch. FROST Quorum is the write path: a roster of signers, an epoch checkpoint, a one-shot authorization slot, and one aggregated Ed25519 signature over an action preimage that lets a specific mutation happen. This page is the read path: several operators independently publishing what they see for one listing, and a report saying whether those observations agree. Nothing here authorizes anything. Both live in crates/trust/chio-federation, under frost and quorum respectively, and no type crosses between the two modules. A third name to keep separate: trust_establishment::QuorumPolicy in the same crate is a conformance-tier floor on a peer handshake, one field, min_tier, and not a count of anything.

What a quorum report settles

An operator who wants to act on a listing published by someone else has a problem the listing itself cannot solve: a single registry that answers can also be the only registry the reader can reach. The answer in the tree is not a protocol. It is a document. chio.federation-quorum-report.v1 collects one observation per publisher for one listing in one namespace, declares the policy those observations were judged against, and carries a final state that a reader can check against the observations without contacting anyone.

The scope is a validator and a schema. validate_federation_quorum_report is a pure function over a struct. It fetches nothing, verifies no signature, resolves no registryUrl, and compares no two observations for agreement. Everything it decides, it decides from the fields in front of it. Nothing else in the workspace constructs a FederationQuorumReport: a repo-wide search finds the module, its own unit tests, an integration smoke test, the reference JSON under docs/standards/, and no producer, no route, and no chio subcommand.

Read that as the boundary between two jobs. Deciding whether mirrors disagree is the aggregator’s job, and it already exists: aggregate_generic_listing_reports in chio-listing groups listings by a divergence key, compares each group against a canonical fingerprint of source digest, status, owner id, and registry URL, and emits a GenericListingDivergence when they differ. Recording that decision so a second operator can audit it is this page’s job. The two types line up almost exactly, and nothing in the tree converts one into the other.


The report, field by field

Every field below is required unless the table says otherwise. The struct carries deny_unknown_fields and rename_all = "camelCase", so an unrecognized top-level key is a parse failure before the validator ever runs.

FieldTypeWhat the validator does with it
schemaStringMust equal chio.federation-quorum-report.v1 exactly. Anything else is UnsupportedSchema, checked first.
reportIdStringNon-empty after trimming. Never parsed, never compared.
generatedAtu64Nothing. It is not compared to any observation timestamp, to any freshness window, or to a clock.
namespaceStringNon-empty. Not normalized, and not matched against any publisher.
listingIdStringNon-empty. Nothing ties it to the reportRef of any observation.
originOperatorIdStringNon-empty, and load-bearing: it is one half of the origin test in the anti-eclipse gate.
quorumThresholdu32Non-zero. Compared against the count of fresh observations, and only in two of the four state arms.
maxReplicaAgeSecsu64Non-zero. The only age ceiling that actually gates: each observation’s ageSecs is compared against this, not against its own maxAgeSecs.
publishersVec<FederationPublisherObservation>Must be non-empty. Operator ids must be unique across the vector.
conflictsVec<FederationConflictEvidence>Optional on the wire, omitted when empty. Divergence keys must be unique; each entry needs a non-empty key and reason and a duplicate-free operator id list.
antiEclipsePolicyFederationAntiEclipsePolicyFour fields, all required. Declared by the report, not by the reader.
finalStateFederationQuorumStateOne of converged, stale, conflicting, insufficient_quorum. Each carries its own consistency rule.
noteOption<String>Omitted from JSON when absent. Never inspected.

SignedFederationQuorumReport is declared as SignedExportEnvelope<FederationQuorumReport>, the same body-plus-key-plus-signature envelope the other four federation contracts use, with sign and verify_signature over the canonical JSON of the body. The alias has no other occurrence in the workspace, and the validator takes the bare body. Signing a report is available; nothing in the tree does it, and validation never asks whether it happened.


One publisher observation

An observation is one operator’s answer to "what does this listing look like from where you stand, and how far from the source are you".

crates/trust/chio-federation/src/quorum.rsrust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FederationPublisherObservation {
    pub publisher: GenericRegistryPublisher,
    pub report_ref: FederationArtifactReference,
    pub observed_listing_sha256: String,
    pub freshness: GenericListingReplicaFreshness,
    pub observed_at: u64,
    pub upstream_hop_count: u32,
}

publisher comes from chio-listing unchanged: role (origin, mirror, or indexer), operatorId, an optional operatorName, a registryUrl, and a possibly empty upstreamRegistryUrls list. Its own validate requires a non-empty operator id and requires the registry URL and every upstream URL to start with http:// or https://. That is the entire URL check: no host resolution, no scheme preference, no reachability.

reportRef is a FederationArtifactReference and carries two constraints the validator enforces by hand. Its kind must be listing_report, and its operatorId must equal the enclosing publisher’s operatorId. That second rule is what stops one operator from submitting an observation whose evidence pointer belongs to another. The rest of the reference is checked for shape and nothing else. validate_federation_artifact_reference asks that schema, artifactId, and operatorId be non-empty and that sha256 be a hex digest, so the reference’s own schema string is never matched against chio.registry.listing-report.v1 even though its kind must be listing_report, and its optional uri is neither validated nor resolved, not even for a scheme.

Both digests run through one helper. The reference’s sha256 and the observation’s observedListingSha256 each pass validate_hex_digest, which requires exactly 64 ASCII hex characters with no surrounding whitespace. A blank value fails one step earlier: validate_hex_digest calls ensure_non_empty first, so an empty or whitespace-only digest comes back as MissingField under the same field name rather than InvalidReference. Uppercase passes despite the error text saying "lowercase-compatible", and hex_digest_helper_preserves_exact_uppercase_compatible_digest_contract pins that on purpose.

observedAt is read by nothing. It is not bounded by generatedAt, not compared to the freshness block that sits beside it, and not ordered against other observations. Treat it as an annotation for a human reader.

Two nested blocks accept unknown fields

FederationPublisherObservation carries deny_unknown_fields, and so do the report, the conflict entry, the anti-eclipse policy, and FederationArtifactReference. GenericRegistryPublisher and GenericListingReplicaFreshness do not: both are declared with rename_all = "camelCase" alone. Unrecognized keys inside publisher and freshness are dropped in silence rather than rejected, so a producer that invents a field there gets no signal that a consumer is ignoring it.

The anti-eclipse policy is four numbers

This is the part that gives the page its name, and it is small enough to read whole.

crates/trust/chio-federation/src/quorum.rsrust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FederationAntiEclipsePolicy {
    pub minimum_distinct_operators: u32,
    pub require_origin_publisher: bool,
    pub require_indexer_observation: bool,
    pub max_upstream_hops: u32,
}

impl Default for FederationAntiEclipsePolicy {
    fn default() -> Self {
        Self {
            minimum_distinct_operators: 2,
            require_origin_publisher: true,
            require_indexer_observation: true,
            max_upstream_hops: 1,
        }
    }
}
FieldDefaultWhat it refuses
minimumDistinctOperators2A report whose unique operatorId count falls below it. Counting is on operator id alone, and duplicates are already a separate rejection.
requireOriginPublishertrueA report with no observation whose role is origin and whose operator id equals the report’s originOperatorId. Both halves are required, so a self-declared origin under a different operator id does not satisfy it.
requireIndexerObservationtrueA report with no observation whose role is indexer. Role only; no operator constraint, and no independence test.
maxUpstreamHops1Any single observation whose upstreamHopCount exceeds it. This rejects the whole report, not the one observation.

The defaults are the shape the profile argues for: an origin, an indexer, at least two distinct operators, and nobody more than one hop from the source. tests/integration_smoke.rs pins all four values and the schema string in one test, federation_defaults_require_multi_party_visibility, which is the only place in the workspace that asserts them.

The report declares the policy it is judged by

validate_anti_eclipse_policy enforces exactly one thing: minimum_distinct_operators must be non-zero. A report may therefore carry requireOriginPublisher: false, requireIndexerObservation: false, maxUpstreamHops at u32::MAX, and minimumDistinctOperators: 1, and the validator will accept it and then hold it to precisely those terms. The defaults are a constructor convenience, not a floor. Reading a federated report means reading its antiEclipsePolicy block first and rejecting the whole document locally if the declared terms are weaker than yours. Nothing in the crate does that comparison for you.

The hop count is self-declared in the same way. upstreamHopCount is never checked against upstreamRegistryUrls, which is the only other place a publisher describes its distance from the source. In the reference fixture the origin declares zero hops and an empty upstream list while the mirror and the indexer declare one hop and one upstream URL each, but that correspondence is a property of the fixture and not of the validator.


The order the validator checks in

Order matters here more than it usually does, because the anti-eclipse gates run before the final state is read, and they reject rather than classify.

rendering
The order validate_federation_quorum_report runs its checks in, every one of which returns on first failure. The three policy gates run between the per-publisher loop and the state arms, so an eclipse condition is a refusal rather than a state.
sourcecrates/trust/chio-federation/src/quorum.rs:89-257at fe56570

The per-publisher loop validates the publisher, the reference, both digests, the freshness block, the hop count, and the operator id, and accumulates four running values: fresh_count, stale_count, has_origin, and has_indexer, plus a set of operator ids. The hop-count check reads the report’s own policy from inside the loop, so a hop violation is found before the policy gates run and reports a different message than they do.

crates/trust/chio-federation/src/quorum.rsrust
if report.anti_eclipse_policy.require_origin_publisher && !has_origin {
    return Err(FederationContractError::InvalidQuorum(
        "anti-eclipse policy requires an origin publisher observation".to_string(),
    ));
}
if report.anti_eclipse_policy.require_indexer_observation && !has_indexer {
    return Err(FederationContractError::InvalidQuorum(
        "anti-eclipse policy requires an indexer observation".to_string(),
    ));
}
if publisher_ids.len() < report.anti_eclipse_policy.minimum_distinct_operators as usize {
    return Err(FederationContractError::InvalidQuorum(
        "insufficient distinct operators for anti-eclipse policy".to_string(),
    ));
}

This is the design decision the page exists to name. An eclipse condition is not a state a report can be in. It is a reason the report is invalid. A reader handed a document whose observations all come from one operator’s mirrors does not receive an insufficient_quorum verdict to weigh; it receives an InvalidQuorum error and has nothing to weigh at all. Three unit assertions cover the three gates: dropping the origin observation, retaining only the non-indexer observations, and raising minimum_distinct_operators to 4 against a three-publisher body each produce InvalidQuorum.


The four final states

FederationQuorumState is a snake-cased enum with four members, and the validator matches on it last. Each arm is a consistency rule between the label and the body, not a computation of the label from the body.

StateAccepted only whenRefusal message
convergedconflicts is empty and fresh_count >= quorumThreshold.converged quorum reports cannot include conflicts; converged quorum reports require fresh observations meeting the quorum threshold
conflictingconflicts is non-empty. Nothing else is required.conflicting quorum reports require conflict evidence
insufficient_quorumThere is a real shortfall. See below for what that reduces to.insufficient_quorum requires a real quorum shortfall
stalestale_count equals the publisher count, so every observation is stale.stale quorum reports require all observations to be stale

The insufficient_quorum arm looks like a four-part test and behaves like a one-part test:

crates/trust/chio-federation/src/quorum.rs235-246rust
FederationQuorumState::InsufficientQuorum => {
    if fresh_count >= report.quorum_threshold
        && publisher_ids.len()
            >= report.anti_eclipse_policy.minimum_distinct_operators as usize
        && (!report.anti_eclipse_policy.require_origin_publisher || has_origin)
        && (!report.anti_eclipse_policy.require_indexer_observation || has_indexer)
    {
        return Err(FederationContractError::InvalidQuorum(
            "insufficient_quorum requires a real quorum shortfall".to_string(),
        ));
    }
}

Three of those four conjuncts are already guaranteed by the gates that ran a few lines earlier. The distinct-operator count cleared its minimum, require_origin_publisher implies has_origin, and require_indexer_observation implies has_indexer. What remains is fresh_count >= quorum_threshold. So a report may call itself insufficient_quorum exactly when its fresh count falls short of its threshold, and never for an anti-eclipse reason, because an anti-eclipse reason already refused the document.

The state is a label, and it is not always unique

The validator refuses labels that contradict the body. It does not derive the label, and for several bodies more than one label survives. A body in which every observation is stale satisfies stale (all stale) and also satisfies insufficient_quorum (fresh count 0, threshold at least 1). A body carrying conflict evidence with too few fresh observations satisfies both conflicting and insufficient_quorum. Two honest producers can publish different states over identical observations and both reports validate. If you compare reports across operators, compare the observation vectors, not the state.

Conflict evidence itself is three fields: divergenceKey, publisherOperatorIds, and reason, all free text apart from the uniqueness rules. The key and the reason must be non-empty, the operator id list must have no empty and no repeated entry, and no two conflict entries may share a divergence key. Nothing checks that the ids listed in a conflict appear among the report’s publishers.


What counts as fresh

Freshness decides the counter that converged and stale are checked against, and the rule is one expression:

crates/trust/chio-federation/src/quorum.rs153-159rust
if publisher.freshness.age_secs > report.max_replica_age_secs
    || publisher.freshness.state == GenericListingFreshnessState::Stale
{
    stale_count += 1;
} else {
    fresh_count += 1;
}

Two consequences follow, and both are easy to get wrong from the type names alone.

The first is that the ceiling is the report’s, not the observation’s. GenericListingReplicaFreshness carries its own maxAgeSecs and validUntil, and its validate only requires maxAgeSecs to be greater than zero and validUntil to be greater than generatedAt. It never compares ageSecs against either. So an observation may declare state: fresh with an ageSecs far past its own maxAgeSecs and still be counted fresh, as long as it stays inside the report’s maxReplicaAgeSecs. The report-level number is the one to set carefully.

The second is that GenericListingFreshnessState has three members and only one of them is tested here. Divergent is not Stale, so a divergent observation inside the age ceiling increments fresh_count and can carry a report to converged.

Divergent counts as fresh here and blocks trust elsewhere

The rest of the tree treats Divergent as the worst of the three states. freshness_state_rank in chio-listing ranks it worst of the three for search ordering, and the trust-activation evaluator refuses outright on it, emitting the ListingDivergent finding with current listing report is divergent and cannot be activated for runtime trust and returning before any further check. The quorum validator makes the opposite call and counts it toward the threshold. Both are shipped, and they disagree. Note also that GenericListingFreshnessWindow::assess, the function that computes a freshness block from a clock, only ever yields Fresh or Stale, so a Divergent value inside a quorum report is one a producer wrote by hand.

A negative fixture pins the interaction from the other direction. Marking two of the three sample observations Stale with age_secs: 400 against a 300-second ceiling, while leaving final_state at Converged, drops fresh_count to 1 under a threshold of 2 and produces InvalidQuorum.


What agreement does not mean

Every observation carries an observedListingSha256, and the validator checks each one is a well-formed 64-character hex digest. It never compares two of them. A report in which the origin, the mirror, and the indexer each observed a different listing body, with an empty conflicts array and three fresh observations against a threshold of 2, validates as converged.

That is not an oversight to route around, it is the division of labour stated at the top. Detecting divergence is the aggregator’s job. aggregate_generic_listing_reports builds a divergence key from actor kind, actor id, and the compatibility source schema and id, groups candidate listings under it, and compares every member against the first one’s fingerprint of source digest, status, owner id, and registry URL. On a mismatch it emits a GenericListingDivergence carrying the divergence key, the actor, the publisher operator ids, and the fixed reason conflicting source artifact, lifecycle state, or namespace ownership across publishers, and drops the group from the results entirely.

FederationConflictEvidence is that output minus the actor fields. The federation contract is the durable, cross-operator record of a decision the listing layer already knows how to make. What is missing between them is the conversion: no function in the workspace turns a GenericListingSearchResponse into a FederationQuorumReport, so an operator publishing one today is transcribing by hand. Capability Discovery covers the aggregator, its ranking, and its freshness gate.


Every refusal this validator can produce

FederationContractError is shared across all five federation contract validators. The quorum path reaches five of its nine variants. The table is exhaustive for this validator, in check order.

ConditionVariantPayload
Schema string is not the v1 constantUnsupportedSchemaThe offending schema string
Blank reportId, namespace, listingId, or originOperatorIdMissingFieldfederation_quorum.report_id and the three siblings
Zero threshold or zero max replica ageInvalidQuorumquorum_threshold must be non-zero; max_replica_age_secs must be non-zero
Empty publisher vectorMissingFieldfederation_quorum.publishers
Zero minimumDistinctOperatorsInvalidQuorumminimum_distinct_operators must be non-zero
Publisher fails its own validateInvalidQuorumpublisher.operator_id must not be empty or publisher.registry_url must start with http:// or https://
Blank schema, artifactId, operatorId, or sha256 inside reportRefMissingFieldfederation_quorum.publishers.report_ref
Malformed reportRef.sha256InvalidReferencefederation_quorum.publishers.report_ref must be a 64-character lowercase-compatible hex digest
reportRef.kind is not listing_reportInvalidQuorumpublisher report_ref must reference a listing report
reportRef.operatorId disagrees with the publisherInvalidQuorumpublisher report_ref operator_id must match publisher.operator_id
Blank observedListingSha256MissingFieldfederation_quorum.publishers.observed_listing_sha256
Malformed observedListingSha256InvalidReferenceThe same field name plus must be a 64-character lowercase-compatible hex digest
Freshness block fails its own validateInvalidQuorumfreshness.max_age_secs must be greater than zero; freshness.valid_until must be greater than generated_at
Hop count above the declared ceilingInvalidQuorumpublisher upstream_hop_count exceeds anti-eclipse policy
Two observations from the same operator idDuplicateValueThe repeated operator id
Three anti-eclipse gatesInvalidQuorumThe three messages quoted above
Blank conflict key or reason; blank id in a conflict listMissingFieldfederation_quorum.conflicts.divergence_key, .reason, .publisher_operator_ids
Repeated id inside one conflict, or repeated divergence key across conflictsDuplicateValuefederation_quorum.conflicts.publisher_operator_ids:{value}, or the repeated divergence key
State contradicts the bodyInvalidQuorumOne of the five messages in the state table

The four variants this path never returns are InvalidExchange, InvalidAdmission, InvalidClearing, and InvalidQualificationCase, which belong to the activation, open-admission, reputation-clearing, and qualification validators. Matching on InvalidQuorum alone is not enough to catch a quorum failure: all five variants above are reachable from this one function, and the distinctions among them live in the payload strings rather than in the type.


Authoring a report that validates

The reference document lives at docs/standards/CHIO_FEDERATION_QUORUM_REPORT_EXAMPLE.json and is not decorative: reference_artifacts_parse_and_validate pulls it in with include_str!, parses it into the struct, and runs the validator over it alongside the other four federation reference documents. If the schema drifts, that test fails. The body below is that file with two of its three publishers elided.

docs/standards/CHIO_FEDERATION_QUORUM_REPORT_EXAMPLE.jsonjson
{
  "schema": "chio.federation-quorum-report.v1",
  "reportId": "fqr-1",
  "generatedAt": 1743552060,
  "namespace": "registry.chio.example/liability",
  "listingId": "listing-liability-provider-1",
  "originOperatorId": "origin-operator",
  "quorumThreshold": 2,
  "maxReplicaAgeSecs": 300,
  "publishers": [
    {
      "publisher": {
        "role": "origin",
        "operatorId": "origin-operator",
        "operatorName": "Origin Operator",
        "registryUrl": "https://origin.chio.example/registry",
        "upstreamRegistryUrls": []
      },
      "reportRef": {
        "kind": "listing_report",
        "schema": "chio.registry.listing-report.v1",
        "artifactId": "report-origin-1",
        "operatorId": "origin-operator",
        "sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
        "uri": "https://origin-operator.chio.example/artifacts/report-origin-1"
      },
      "observedListingSha256": "1111111111111111111111111111111111111111111111111111111111111111",
      "freshness": {
        "state": "fresh",
        "ageSecs": 30,
        "maxAgeSecs": 300,
        "validUntil": 1743552360,
        "generatedAt": 1743552030
      },
      "observedAt": 1743552030,
      "upstreamHopCount": 0
    }
  ],
  "antiEclipsePolicy": {
    "minimumDistinctOperators": 2,
    "requireOriginPublisher": true,
    "requireIndexerObservation": true,
    "maxUpstreamHops": 1
  },
  "finalState": "converged",
  "note": "Requires origin plus independent mirror/indexer observation before a remote listing is treated as converged."
}

The full fixture adds a mirror at mirror-operator-a and an indexer at indexer-operator-a, both declaring one upstream registry URL and upstreamHopCount: 1, and all three publishers report the same observedListingSha256. Three fresh observations against a threshold of 2, three distinct operators against a minimum of 2, an origin whose operator id matches originOperatorId, an indexer present, no hop above 1, and an empty conflict list: converged.

Four things to get right when you build one. The digest fields need exactly 64 hex characters and no padding; a trimmed comparison is not applied, and hex_digest_validation_rejects_padded_digest pins that a space on either side fails. Each reportRef.operatorId must repeat its own publisher’s operator id. The origin observation needs both the origin role and the exact operator id named in originOperatorId, which is the rule most likely to be missed under a default policy. And every observation’s ageSecs is judged against maxReplicaAgeSecs on the report, so setting a generous per-observation maxAgeSecs buys nothing.


Why this is filed here and not under Swarm

Chio has two joint-authorization mechanisms on the write path, and a quorum report fires neither. Under Bilateral Co-Sign two kernels sign one receipt and a verifier refuses the invocation without both. Under FROST Quorum a threshold roster produces one aggregated signature bound to an action digest, a roster digest, a key epoch, a resource fence, and an issue and expiry window, over one of eight authorization domains, seven of which map to a registered action class, including chio.frost.settle-commitment.v1 and chio.frost.credentials-passport-revoke.v1. Both answer "may this mutation proceed". A quorum report answers "do independent observers describe the same thing", and its acceptance changes nothing anyone may do.

The federation profile is explicit about the boundary that keeps those separate. Its non-goals list rules out "mirror or indexer visibility as ambient runtime trust", and FederationImportControl carries it in code: five booleans, every one of which the activation validator requires to be true, including explicitLocalActivationRequired and prohibitAmbientRuntimeAdmission. A converged report widens what an operator can see. Turning that into runtime trust is a separate, reviewed, locally signed step covered under Federation.

The report does appear as evidence in one other contract. FederationQualificationMatrix carries a required non-empty quorumReportRef alongside an exchange reference and a reputation-clearing reference, and its validator refuses any matrix that does not cover all five requirement ids TRUSTMAX-01 through TRUSTMAX-05. Two of the six scenario kinds it can label a case with, insufficient_quorum and eclipse_attempt, name this page’s subject. The reference matrix maps them to TRUSTMAX-02 and TRUSTMAX-05. The reference is a string; nothing resolves it to a report.


Guarantees and limits

StatusClaimEvidence
ShippedOne schema constant, one struct family, one validator. A report names a listing in a namespace, carries at least one publisher observation, and declares both an anti-eclipse policy and a final state.CHIO_FEDERATION_QUORUM_REPORT_SCHEMA and validate_federation_quorum_report in crates/trust/chio-federation/src/quorum.rs
ShippedThree anti-eclipse gates run before the final state is read, so a missing origin, a missing indexer, or too few distinct operators refuses the document rather than grading it.The three InvalidQuorum returns between the publisher loop and the match report.final_state in quorum.rs
ShippedDuplicate publishers are impossible. Operator ids are inserted into a set and a repeat is DuplicateValue, so the distinct-operator count and the publisher count are always equal.publisher_ids.insert(...) in the publisher loop
Proved by testRemoving the origin observation from an otherwise valid report is refused, as is retaining only the non-indexer observations, as is raising the distinct-operator minimum above the publisher count.quorum_report_requires_origin_publisher and quorum_report_rejects_invalid_observations_and_policy_failures in src/tests.rs
Proved by testEleven negative fixtures cover the observation path: wrong schema, zero threshold, zero max age, empty publishers, wrong reference kind, mismatched reference operator, malformed digest, hop count over the ceiling, duplicate operator, missing indexer, and an unreachable operator minimum.quorum_report_rejects_invalid_observations_and_policy_failures
Proved by testSix negative fixtures cover the conflict and state path, including duplicate divergence keys, conflicts under a converged state, a converged state below the fresh threshold, a conflicting state with no evidence, an insufficient-quorum claim with no shortfall, and a stale state with a fresh observation.quorum_report_rejects_invalid_conflict_and_state_combinations
Proved by testThe reference JSON parses into the struct and passes the validator, and the four policy defaults plus the schema string are asserted independently.reference_artifacts_parse_and_validate; federation_defaults_require_multi_party_visibility in tests/integration_smoke.rs
LimitThe policy is self-declared. validate_anti_eclipse_policy enforces only that minimum_distinct_operators is non-zero, so a report may switch off the origin and indexer requirements and raise the hop ceiling, and it will be judged by those terms.validate_anti_eclipse_policy in crates/trust/chio-federation/src/validation.rs
LimitObservations are never compared. Three different observedListingSha256 values with an empty conflict list validate as converged. Divergence detection belongs to the caller.observed_listing_sha256 appears in quorum.rs only in the struct and in one validate_hex_digest call
LimitA Divergent observation inside the age ceiling counts as fresh and can carry a report to converged, while the trust-activation evaluator refuses runtime trust on the same value.The == GenericListingFreshnessState::Stale test in quorum.rs; the ListingDivergent arm in crates/economy/chio-listing/src/trust_activation.rs
LimitThe final state is not derived and is not always unique. An all-stale body validates as either stale or insufficient_quorum; a conflicted body below threshold validates as either conflicting or insufficient_quorum.The four arms of match report.final_state, each of which tests only for contradiction
LimitupstreamHopCount is asserted by the publisher and never cross-checked against upstreamRegistryUrls, and observedAt is read by nothing at all.Neither field appears outside its struct definition and, for the hop count, one comparison against the declared ceiling
LimitThere is no operator independence model. Publishers are distinct when their operator ids differ, and two operator ids controlled by one party satisfy the default minimum of two.The publisher set is keyed on operator_id; compare FederatedReputationInputReference::issuer_independence_group_id, which the sibling reputation contract does carry
Not wiredNothing produces a quorum report. No route, no CLI subcommand, and no other crate builds or consumes FederationQuorumReport. Publishing one is a document an operator assembles.Repo-wide search for the type and the schema string returns the module, its tests, tests/integration_smoke.rs, the README, and the two files under docs/standards/
Not wiredNo metric counts quorum outcomes. The crate’s metrics module registers a federation-hop counter and a latency histogram driven by bilateral co-signing, and nothing else.crates/trust/chio-federation/src/metrics.rs: CHIO_FEDERATION_HOP_TOTAL and CHIO_FEDERATION_HOP_LATENCY_SECONDS, incremented from co_sign_with_origin
UnsupportedSignature verification as part of validation. SignedFederationQuorumReport exists as a type alias and has no other occurrence in the workspace; the validator takes the bare body and never asks who signed it.The alias in quorum.rs; SignedExportEnvelope in crates/core/chio-core-types/src/receipt/lineage.rs
UnsupportedAuthorizing anything. A quorum report grants no capability, opens no lease, and gates no mutation. Joint authorization is FROST or bilateral co-signing, and no type crosses between those modules and this one.frost::FrostAuthorizationBodyV1 and quorum::FederationQuorumReport share no type and no import

Next Steps

  • FROST Quorum · the write-path quorum this page is deliberately not: roster, epoch checkpoint, one-shot slot, and the seven action classes registered across its eight authorization domains
  • Federation · the five federation contracts side by side, and the import controls that keep visibility from becoming runtime trust
  • Capability Discovery · the aggregator that actually detects divergence across mirrors, and the freshness gate feeding it
  • Revocation Oracle · the other cross-operator read path, where a signed epoch root and a reader-set freshness window replace a publisher count
  • Portable Reputation · the sibling contract that does carry an issuer independence group, and the sybil controls built on it
Quorum & Anti-Eclipse · Chio Docs