Skip to content

security: S5 recidivism is per-model, so a validator can spread missed votes across models and never escalate #193

Closed

Description

@umeradl

Summary

S5 recidivism escalation (full MIN_STAKE slash + jail) is counted per slasher contract, i.e. per model. A validator registered on several models can spread liveness faults across them so no single model's counter ever reaches s5RecidivismThreshold.

DinValidatorStake.slashPartial records each partial slash in _partialSlashGIs[validator][msg.sender] (foundry/src/DinValidatorStake.sol:309), where msg.sender is the calling DINTaskCoordinator / DINTaskAuditor. With the defaults (window 5 GIs, threshold 3, DinValidatorStake.sol:196-198), a validator can miss up to 2 votes/submissions per model per window on every model it serves. Across N models that is (threshold − 1) × N misses per window without S5 ever firing, and isValidatorActive stays true throughout.

Why it is keyed per contract today

This is deliberate, not an oversight. giIndex is a per-model counter, so different models' GI sequences interleave arbitrarily. A single shared ring per validator would receive non-monotonic values and underflow the ascending-order trim in _trimAndRecordPartialSlash (see the NatSpec on _partialSlashGIs, DinValidatorStake.sol:136-146). Any fix has to keep that safety property.

Impact

A validator can stay chronically unreliable across the network while only ever paying the S2 partial slashes (s2SlashFractionBps, 30% of minStake per miss by default). The escalation that is meant to eject repeat offenders never triggers. The per-miss partial slashes still apply, so this is a cost-reduction gap, not a free pass.

Options

  • Model-independent window: track partial slashes per validator in a time-based (block timestamp) or platform-level sequence window that is monotonic across all slashers, and escalate on the aggregate count.
  • Two-level check: keep the per-slasher ring for per-model escalation and add a global per-validator counter with its own threshold.
  • Accept and document it as a trust assumption, with the per-model threshold calibrated so that (threshold − 1) × typical model count misses per window is tolerable.

Tracking

Row 11 (KNOWN GAP) of the adversarial threat model, Developer/design/adversarial-threat-model.md (task_240926_17, merged via PR No. 182). Part 2's test_knownGap_… for Row 11 should flip to test_defended_… when this is fixed.

Activity

  1. umeradl commented on Sep 30, 2026

    @umeradl
    MemberAuthor

    Status update: not closable yet

    Checked on origin/develop @ 6ccc28c. No fix and no decision yet.

    Item Status Evidence
    Decide: a window independent of the model, a two-level counter, or accept and document ❌ Open
    Fix ❌ The S5 ring is still keyed per slasher: _partialSlashGIs[validator][msg.sender] (DinValidatorStake.sol:309, :317)
    Threat model records the gap ✅ Developer/design/adversarial-threat-model.md Row 11 (KNOWN GAP)
    test_knownGap_… for Row 11 flips to test_defended_… ❌ P3Adversarial.t.sol isn't written yet (issue No. 154, Part 2)

    Remaining

    1. Choose an option. Any fix has to keep the ascending-order trim safe across models' GI sequences, which interleave.
    2. Implement it, or document it as a trust assumption with a calibrated threshold. Then update Row 11.
  2. added a commit that references this issue on Oct 7, 2026
  3. added a commit that references this issue on Oct 25, 2026
  4. abrahamnash commented on Oct 25, 2026

    @abrahamnash
    Member

    Fixed by #236 (merged to develop on 25 Oct 2026): S5 now also counts missed duties across all models within a time window, with an escalation epoch so no miss counts toward two penalties. Closing manually because "Closes #193" only auto-closes on merges to the default branch (main).

    Follow-ups: default threshold calibration is tracked in #155, and the penalty-severity decision in #274.

  5. umeradl commented on Oct 29, 2026

    @umeradl
    MemberAuthor

    @Santiagocetran @robertocarlous could one of you run the closure check on this (din-close-issue 193)? PR No. 236 says "Closes" this issue and was merged into develop on 25 Oct, and Abraham's comment above says it's fixed, but the issue is still open. I can't close it myself because I implemented it (MAINTAINING No. 5.2).

  6. umeradl commented on Oct 29, 2026

    @umeradl
    MemberAuthor

    Pre-check for whoever closes this. My verification on origin/develop @ 6e73dca says it's closable. I can't close it myself because I implemented it (MAINTAINING No. 5.2), so @Santiagocetran / @robertocarlous, please confirm and close.

    • Fix (option 2, two-level check): PR No. 236, merged into develop as 55c4b52 (push CI green). DinValidatorStake.slashPartial keeps the per-slasher ring and also records every partial slash in a time-windowed, per-validator global ring (_trimAndRecordGlobalPartialSlash, L357-367). It escalates on either level. The default is 7 days / 6 misses, settable through setS5GlobalParams, and 0 turns it off. The global ring uses timestamps, not per-model GI numbers, so the ascending-order trim stays safe across models.
    • No double counting: escalation increments s5EscalationEpoch[validator] and clears the caller's ring and the global ring, so a miss can't count toward two penalties.
    • Tests: foundry/test/SlashingInvariants.t.sol has seven test_s5Global_* tests (escalates across slashers, settles the other slashers' rings, two slashers of one model, window expiry, off at 0, setter validation, defaults). DeployPlatform.t.sol and DeployPlatformScript.t.sol cover the deploy defaults and overrides. Local forge test on develop f5b2284: 516 passed, 0 failed.
    • Threat model: Developer/design/adversarial-threat-model.md L172 marks Row 11 "Flipped by task_061026_20 Part C".
    • ↪️ The issue's "flip test_knownGap_… to test_defended_…" line: P3Adversarial.t.sol doesn't exist yet (issue No. 154, Part 2), so there's no known-gap test to flip. Row 11's test should be written as a defended case when that suite lands.

    Follow-ups, already tracked: default threshold calibration in issue No. 155, and the S5 penalty severity in issue No. 274 (PR No. 286, still open).

  7. Santiagocetran commented on Oct 29, 2026

    @Santiagocetran
    Collaborator

    @umeradl, I checked the implementation and CI evidence on develop at 1f1431c. The tests cover counting misses across models, triggering S5 escalation, expiring old misses and preventing already-counted misses from causing another penalty. The relevant regressions passed in CI as part of the 516 passing Foundry tests. This was source/test inspection and CI verification; I didn’t run tests locally, and full-GI integration was skipped.

    Based on that evidence, the cross-model recidivism bug is fixed and I recommend closing #193 for the implemented fix. Following the task plan’s sequencing, the remaining Row 11 defended adversarial test stays tracked under #154 and is intended for PR #218; it is not yet completed or merged.

    CI evidence · Task sequencing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions