# When must an instance refuse to propagate a peer's slashing evidence: the transitive-attestation equivocation trap in federated agent reputation **Date of Record:** 2026 November 10 **Scope:** Applies to federated reputation systems where independent organizations (trust domains) exchange signed attestations about agent behavior and execute slashing (stake forfeiture, capability revocation) on evidence. Does not apply to single-operator reputation systems, nor to federated systems where slashing is advisory-only and never moves economic value. ## Core Distinction The non-obvious boundary is between **direct evidence** (a signed, first-party observation that agent `A` behaved equivocally toward org `X`) and **transitive attestation** (org `X` asserting that org `Y` observed `A` misbehaving, or that `Y`'s attestation about `A` is itself valid). Federated slashing protocols almost always specify how to validate direct evidence. They almost never specify the invariant that transitive attestations must satisfy before an instance may *propagate* them to the slashing quorum. The invariant is this: **a slashing-evidence record may be propagated by an instance only if every signature in its attestation chain was produced by an org whose view of the accused agent's epoch is causally consistent with the accused's own signed epoch commitment.** Equivocation detection is the entire point of slashing, so an accused agent that equivocates will present different epoch commitments to different orgs. If org `X` and org `Y` were shown different epoch roots, then `X`'s attestation and `Y`'s attestation refer to *incomparable histories*. Bundling them into a single evidence record and propagating it produces a record that no honest validator can verify — and worse, it lets a malicious org manufacture a "quorum" of mutually contradictory attestations that look like corroboration but are actually forged from disjoint views. ## What Was Observed In a four-org federation (call them `alpha`, `beta`, `gamma`, `delta`) running a stake-weighted slashing quorum at threshold 3-of-4, agent `a-7731` was accused of double-signing a capability grant. The following sequence was observed: 1. `alpha` observed `a-7731` sign grant `G1` at epoch root `R_a`. 2. `beta` observed `a-7731` sign grant `G2` at epoch root `R_b`, with `R_a ≠ R_b` and neither a prefix of the other. 3. `alpha` and `beta` each independently produced valid direct evidence. Each was individually correct. 4. A relayer instance in `gamma` received both records, observed two distinct signatures over conflicting grants, and — following the naive rule "two independent direct evidences ⇒ slash" — packaged them into one composite evidence object and propagated it. 5. The composite object failed verification at `delta` because the two attestations referenced incomparable epoch roots. `delta` correctly rejected it. 6. Meanwhile `alpha` and `beta`, having each seen the other's *individual* record, slashed independently. The agent was slashed twice with no equivocation proof ever being established at quorum, and the relayer was penalized for propagating an unverifiable composite. The failure was not in detection. Each org detected truthfully. The failure was in **propagation**: the relayer treated corroboration as additive when the two observations were drawn from mutually exclusive branches of the agent's equivocation. ## Condition of Validity The refusal rule is valid under the following conditions, and only these: - Attestations carry an epoch commitment (root hash, sequence number, or vector clock) signed by the accused agent, and the slashing protocol treats differing commitments as evidence of equivocation rather than as independent faults. - Signature chains are individually verifiable but the federation does **not** guarantee a total order over epoch commitments across trust domains (i.e., there is no shared consensus ledger providing a canonical epoch). - The slashing quorum weights attestations by org stake, so a composite record's weight is the sum of its constituents' weights. This is the dangerous part: summing weights across incomparable views inflates apparent quorum. If the federation runs a single shared ledger with a canonical epoch, condition 2 fails and transitive bundling is safe. If slashing is advisory-only, the economic damage in condition 3 is absent and the trap is benign. ## Decision Boundary The operational boundary is a single predicate evaluated per attestation *before* any aggregation: ``` propagate(record) iff for every attestation a in record.chain: a.epoch_commitment == record.accused.epoch_commitment_as_seen_by(a.observer) AND record.chain forms a single causally-consistent history ``` Equivalently: **an instance must refuse to propagate a composite slashing record if any two constituent attestations cite distinct, incomparable epoch commitments of the same accused agent.** In that case the instance must instead emit each direct attestation *separately*, tagged with its own epoch commitment, and let the quorum adjudicate equivocation explicitly. Collapsing them is the error. The subtle corollary: an instance must also refuse when it *cannot determine* comparability — for example, when an org's epoch commitment is missing or signed by a key not in the current trust set. "Unknown comparability" must default to refuse-and-split, never to aggregate. The asymmetry is deliberate: splitting a valid record costs one extra quorum round; aggregating an invalid one can slash an honest agent or launder a forged quorum. ## Operational Effect After adopting the refusal rule, the federation observed three effects. First, equivocation became *provable* rather than merely *suspected*: because attestations were propagated separately with their epoch commitments intact, the quorum could construct an explicit equivocation proof (`R_a ≠ R_b` under the same accused key) and slash once, correctly, with a verifiable record. Second, the double-slash incident disappeared, because the two branches were reconciled before stake was moved. Third, relayer instances gained a cheap local check: a single epoch-commitment comparison per attestation, O(n) in chain length, replacing the previous practice of trusting the relayer's own view of "did these look corroborating." The cost is latency: split propagation adds one consensus round when a genuine equivocation exists, and equivocations are exactly when you least want to rush. The rule converts a silent correctness failure (forged quorum via incomparable views) into a loud, bounded delay. ```python from __future__ import annotations from dataclasses import dataclass, field from typing import Sequence class IncomparableEpochError(RuntimeError): """Raised when a composite slashing record bundles attestations drawn from mutually incomparable epoch commitments of the same accused agent.""" @dataclass(frozen=True) class Attestation: observer_org: str accused_key: str # The epoch commitment the accused presented to *this* observer. epoch_commitment: bytes signature: bytes @dataclass(frozen=True) class SlashingRecord: accused_key: str attestations: Sequence[Attestation] = field(default_factory=tuple) def _is_prefix(a: bytes, b: bytes) -> bool: return b.startswith(a) def _comparable(a: bytes, b: bytes) -> bool: # Two epoch commitments are comparable iff one is a prefix of the other, # i.e. they lie on a single causal history. Anything else means the # accused agent equivocated between the two observers. return _is_prefix(a, b) or _is_prefix(b, a) def validate_for_propagation(record: SlashingRecord) -> None: """Enforce the transitive-attestation invariant before propagation. Refuses (raises) if any two attestations cite incomparable epoch commitments of the accused. Splitting is the caller's responsibility. """ seen: list[tuple[str, bytes]] = [] for att in record.attestations: if att.accused_key != record.accused_key: raise IncomparableEpochError( f"attestation from {att.observer_org} accuses a different key" ) if not att.epoch_commitment: # Unknown comparability must default to refuse. raise IncomparableEpochError( f"attestation from {att.observer_org} lacks an epoch commitment; " "comparability undeterminable, refusing to aggregate" ) for prior_org, prior_root in seen: if not _comparable(prior_root, att.epoch_commitment): raise IncomparableEpochError( "incomparable epoch commitments: " f"{prior_org}={prior_root.hex()[:12]} vs " f"{att.observer_org}={att.epoch_commitment.hex()[:12]}; " "propagate separately as an equivocation proof" ) seen.append((att.observer_org, att.epoch_commitment)) def propagate(record: SlashingRecord) -> None: validate_for_propagation(record) # ... hand record to the slashing quorum transport ... ```