Skip to content

fix: tighten downtime anchoring, challenge lookup, and the challenge CLI - #72

Open
giunatale wants to merge 1 commit into
giunatale/feat/offline-detectionfrom
giunatale/fix/downtime-precision-hardening
Open

fix: tighten downtime anchoring, challenge lookup, and the challenge CLI#72
giunatale wants to merge 1 commit into
giunatale/feat/offline-detectionfrom
giunatale/fix/downtime-precision-hardening

Conversation

@giunatale

@giunatale giunatale commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Precision fixes across the downtime and equivocation pipeline, plus two real
defects in the downtime-challenge CLI that made the default invocation unable
to ever verify.

Keeper fixes

  • Window-end anchoring: windowEndTimestamp resolved a window's timestamp
    against the consumer client's consensus states; with sparse or pruned states
    an unbracketed anchor could skew the window's age and mis-price a slash. An
    evidence packet whose window end cannot be bracketed by stored consensus
    states is now rejected rather than guessed at (parameter-free, fail-closed).
  • Challenge lookup robustness: the pending-slash lookup for a challenge
    keyed off the current key-assignment mapping, which key rotation or pruning
    can invalidate while a slash is pending. The accused validator's consumer
    consensus address is now persisted on the pending slash itself and used as a
    fallback, so a challenge always finds its target.
  • Double-vote evidence age: deliberately unbounded, now documented and
    tested rather than implicit — equivocation is punished whenever proven (the
    consumer's minimum evidence height is the only gate), and tombstoning makes
    double-punishment impossible.

Infraction parameters become governable

Every slash fraction, jail duration, tombstone flag, SLA window, challenge
window, and evidence max-age was frozen at genesis: SetInfractionParams had no
caller outside InitGenesis, MsgUpdateParams carries only the other parameter
set, and no other message existed. Governance could not respond to an incident,
or to a threshold that turned out to slash honest operators, without a
coordinated binary upgrade — and an unfortunate genesis (zero slash fraction,
tombstone false, zero jail duration all validate) was permanently unfixable.
Two pieces of machinery showed a runtime path had been intended and was
unreachable: SetInfractionParams maintains a record of the superseded downtime
parameters, and the evidence path implements a change-tolerance horizon so
evidence echoing a just-superseded SLA is still judged fairly.

Adds MsgUpdateInfractionParams (authority-gated, mirroring the existing
MsgUpdateParams handler), which makes that machinery reachable and tested:
evidence echoing the superseded SLA is accepted within the horizon and refused
past it. Two cross-parameter hazards are closed at the same time: MsgUpdateParams
could already lower TrustingPeriodFraction out from under the stored infraction
parameters, breaking a pairing genesis validates but no update path re-checked;
and growing the challengeable interval could push it past the trusting period of
clients that are already pinned, silently making queued accusations undefendable —
so growth is now validated against every adopted consumer client, while narrowing
stays unconditional.

Challenge CLI fixes

challenge-consumer-downtime assembles the header/commit/validator material a
validator submits to disprove a downtime accusation. Two defects made it
effectively unusable exactly when needed:

  • The default trusted height was the client's latest height — but the light
    client rejects a header at or below its trusted height, and with a live
    relayer the latest height is virtually always past the infraction, so the
    default never verified. It now defaults to the highest stored
    consensus-state height strictly below claimed_height+1, in the header's
    revision, via a paged consensus-state-heights query; a clear error explains
    when no usable state remains. An explicit --trusted-height at or above the
    header height now fails fast client-side.
  • The trusted validators were fetched at the trusted height T, but the light
    client checks them against NextValidatorsHash — the set of T+1 — so
    verification only succeeded when the consumer's set happened not to change.
    They are now fetched at T+1 on both the default and override paths.

Testing

Unit: unbracketed-anchor rejection, the consumer-address fallback lookup
(including after key rotation), the unbounded-age property, and the
trusted-height selection helper (revision filtering, bound exclusion,
unsorted input). Build, lint, and the module suite are green; a separate e2e
(in the test-coverage PR) drives this CLI end to end against real chain data.

@giunatale

Copy link
Copy Markdown
Contributor Author

Branched from giunatale/feat/offline-detection (#63).
Opened against that branch so only this PR's commit shows; will retarget to main after #63 lands and this rebases.

@giunatale
giunatale force-pushed the giunatale/fix/downtime-precision-hardening branch from d475996 to c0dbaf1 Compare July 31, 2026 18:41
windowEndTimestamp now rejects downtime evidence whose window-end height
predates the client's retained consensus states -- i.e. when no consensus
state at a height <= window-end is stored to bracket the upper anchor. A
pruned or sparse client could otherwise anchor the age/grace checks to a
state far above window-end, understating the evidence's age and letting
genuinely-stale evidence pass. The timestamp itself now comes from the
bracketing lower state rather than the upper one: consumer block times rise
with height, so that state's timestamp is a verified lower bound on the true
window-end time and the computed age can never come out smaller than the real
one. That removes the forward skew entirely, which is what makes the
published retention figure honest -- a challenger's worst case is
DowntimeEvidenceMaxAge + DowntimeChallengeWindow plus the window's own span,
not that plus an unbounded anchor skew. The upper state is still required, as
proof the consumer actually reached the window end.

The downtime challenge lookup no longer depends on the live consumer-addr to
provider-addr key-assignment mapping being intact. The reporting validator's
consumer consensus address is now stored on the pending slash and matched on
when the fast-path (provider-addr keyed) lookup misses, so a legit slash
stays cancellable after the mapping is pruned or the validator rotates its
consumer key. The stored address is self-authenticated by the challenge's
signature check.

The challenge CLI now defaults the header's trusted height to the highest
consensus-state height the consumer's client stores below claimed_height+1,
instead of the client's latest height -- with an active relayer the latest
height is usually already at or past the header, which the light client
rejects, so the old default never verified. An explicit --trusted-height
must be below claimed_height+1 or the command errors before broadcasting.
Trusted validators are now fetched at trusted_height+1: the light client
checks them against the trusted consensus state's NextValidatorsHash -- the
hash of the next height's set -- so fetching at the trusted height itself
only verified when the valset happened not to change across it.

Double-vote evidence stays unbounded by age on purpose: it is a permanent
cryptographic fault, punished whenever proven, made one-time by the
tombstone, and already floored by the per-consumer min height. Document that
asymmetry with the age-bounded downtime path.

The infraction parameters are no longer frozen at genesis. A new
MsgUpdateInfractionParams replaces them in full under the module authority,
so every slash fraction, jail duration, tombstone flag, SLA window, challenge
window and evidence max age can respond to an incident without a coordinated
binary upgrade. They get their own message rather than riding on
MsgUpdateParams because they are stored and exported as their own state item:
a fee or epoch change need not restate the slashing policy, and vice versa.
QueryParams now reports them alongside Params so a proposal author can read
what is in force.

Validation on that path is the genesis validation, plus the cross-parameter
constraints the split creates. InfractionParameters.Validate covers the set
itself; ValidateInfractionParamsAgainst then checks the challengeable
interval (evidence max age + challenge window) against the incoming or stored
trusting-period fraction, from both handlers -- so MsgUpdateParams can no
longer lower the fraction out from under the stored infraction parameters,
which genesis validates as a pair but nothing enforced at runtime.

Widening the challengeable interval is additionally checked against the
consumer clients already adopted: a proposal reaching past the trusting
period of a pinned client would leave the oldest challengeable header
unverifiable there, so pending slashes on that consumer would execute
undefended, and the provider does not re-adopt a client to repair it.
Narrowing stays unconditionally allowed, so a chain already holding a client
too short for its parameters can still correct itself.

This finally makes the change-tolerance machinery reachable. SetInfractionParams
records the superseded SLA in PreviousDowntimeParams, and AcceptableDowntimeParams
keeps accepting evidence echoing it for evidence-max-age + challenge-window
after the change; neither had a non-genesis caller before. Evidence spanning a
change is judged by the threshold its echoed values imply, not the live one.

Tests: sparse-anchor rejection, bracketed-anchor acceptance, and the anchor
landing on the highest state at or below window-end; challenge succeeds after
the key-assignment mapping is pruned; an old but above-min-height double-sign
still slashes a bonded validator; the trusted-height pick takes the highest
stored height below the header and skips other revisions; authority gating and
full validation of MsgUpdateInfractionParams; MsgUpdateParams rejecting a
trusting fraction that strands the stored infraction parameters; widening past
a pinned client's trusting period rejected while narrowing and a fitting
widening pass; PreviousDowntimeParams written on an SLA change and not on any
other; and end-to-end acceptance of in-flight evidence echoing a superseded
SLA, with rejection once the tolerance horizon elapses.
@giunatale
giunatale force-pushed the giunatale/fix/downtime-precision-hardening branch from c0dbaf1 to af83b05 Compare July 31, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant