paritytech / paritytech/web3-storage

RFC: Forced re-stake deadline + auto-eviction after a slash

Open
#387 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
12
Forks
3
Avg merge
2d 2h
Merged PRs (30d)
33

Description

RFC: Forced re-stake deadline + auto-eviction after a slash

Follow-up to the companion RFC #386, "Size provider stake to economic
exposure, not just physical bytes." That RFC fixes
how much stake a provider needs going forward; this one fixes what happens
to a provider's other, uninvolved clients once that provider is
slashed. A related but separable gap. It's filed on its own since it's a
bigger behavioral change (automatic eviction of live agreements) and
shouldn't block the more straightforward formula fix.

In plain terms

Current condition. When a provider fails a challenge, their entire
stake gets wiped to zero in one shot, as punishment. But nothing else
happens: they stay listed as a normal provider and keep every other
agreement they hold with completely unrelated clients, and nothing forces
those other clients to notice anything changed.

Why that's dangerous. Once a provider has zero stake, there is nothing
left for the chain to threaten them with. Every other client they still
serve, who did nothing wrong and had no part in whatever got the provider
slashed, is now relying on a guarantee backed by nothing. The only fix
today is each of those clients individually noticing and manually removing
the provider from their own bucket, one at a time, with no deadline forcing
it to happen.

After the fix. The moment a provider falls below the stake it needs, a
clock starts. If they don't top back up within that window, the chain
automatically evicts them from every remaining agreement and refunds each
client's locked payment, instead of leaving that to chance and individual
vigilance.

Summary

A failed challenge zeroes a provider's entire stake. By design, this is
the deterrent (isolating stake per-bucket would reopen a
cheat-where-nobody's-watching arbitrage). But nothing else changes:
accepting_primary stays whatever it was, every other open agreement stays
active and unflagged, and stake sufficiency is never rechecked against
agreements that already exist. The only path back to safety for those
bystanders is remove_slashed, called individually, per bucket, by whoever
happens to notice.

This RFC proposes a bounded grace period to re-stake after a slash, with
automatic bulk eviction of remaining agreements if the provider doesn't.

Related: #310 (providers that never store anything). A provider that has
just been slashed to zero is, from that point forward, economically
identical to the disposable-freeloader profile #310 describes, for every
relationship it hasn't been caught on yet, except now it's guaranteed to
have zero stake rather than merely under-provisioned stake.


The gap

slash_provider_for_failed_challenge (challenges.rs:225-274) zeroes the
provider's entire stake. But:

  • accepting_primary/accepting_extensions are untouched. The provider
    looks completely normal to new prospective clients and to existing ones.
  • Every other open agreement stays active, unflagged, with no on-chain signal
    forcing anyone to notice.
  • remove_slashed (lib.rs:1496-1554) exists, but it's opt-in and
    per-bucket: someone has to find out and act, the same passivity problem
    that lets the underlying freeloading strategy work in the first place.

A provider that has just been slashed to zero has no further chain-side
deterrent left for any relationship it hasn't been caught on yet. The stake
was a one-time, non-renewable stick; once spent, the chain currently gives
nothing back to force a resolution.

Proposed change

Reuse the pattern already in this pallet for deregister_at
(lib.rs:1083-1107): an announcement deadline on ProviderInfo, drained by a
bounded on_initialize sweep (the codebase already has this shape for
challenge timeouts, see LastSweptChallengeBlock).

  1. On slash, if the provider's stake now falls below required_stake for its
    current book, set a new field, understake_since: Option<BlockNumberFor<T>>, and flip accepting_primary/
    accepting_extensions to false immediately (same as deregister_provider
    already does), so at minimum no new exposure can accumulate on top of an
    already-broken guarantee.
  2. Start a bounded deadline: understake_deadline = now + T::DeregisterAnnouncementPeriod.
  3. If the provider calls add_stake before the deadline and clears
    required_stake again, clear understake_since and restore acceptance
    flags to their prior values.
  4. If the deadline passes without sufficient stake, a bounded per-block sweep
    (mirroring the existing challenge-timeout sweep) evicts remaining
    agreements: effectively an automatic, bulk remove_slashed across every
    bucket that provider still holds, refunding each payment_locked to its
    owner instead of requiring each bystander to find out and act
    individually.

Design decisions

  1. Grace period: reuse DeregisterAnnouncementPeriod directly, no new
    config.
    Rather than introduce a separate tunable, reuse the existing
    constant. It already satisfies DeregisterAnnouncementPeriod > ChallengeTimeout via the genesis-time integrity check at
    lib.rs:155-159, which is exactly the property this
    window needs (long enough that a legitimate provider isn't evicted before
    a live challenge could even resolve). One fewer independent parameter to
    reason about and mistune.
  2. Sweep bounding: identical shape to the existing challenge-timeout
    sweep.
    New storage, UnderstakeDeadlines: StorageDoubleMap<deadline, index, AccountId>, drained the same bounded, cursor-based way
    Challenges/LastSweptChallengeBlock already are. No new sweep
    architecture, just the same pattern applied to a second deadline type.
  3. Trigger generically, not just from the slash path. A single
    check_understake(provider) internal function, called deterministically
    from slash_provider_for_failed_challenge (guaranteed path), and also
    exposed as a permissionless extrinsic anyone can call (mirroring
    remove_slashed's permissionless design) to cover cases where a provider
    falls under-stake without being slashed (for example, a governance
    change to the exposure ratio from the companion RFC), without needing an
    unbounded chain-wide scan of every provider on every such change.

Dependency note

This RFC assumes the companion "exposure-based stake" RFC's required_stake()
helper exists (step 1 above references "required_stake for its current
book," which after that RFC includes exposure, not just bytes). If this RFC
lands first for some reason, required_stake here should be read as
whatever the current bytes-only formula is at the time. The grace-period and
auto-eviction mechanism is independently useful either way; it doesn't
depend on which stake formula is in effect, just that some formula exists to
check against.

Touch surface

  • crates/pallets/storage-provider/src/lib.rs:
    • ProviderInfo: new field understake_since: Option<BlockNumberFor<T>>.
    • New storage: UnderstakeDeadlines: StorageDoubleMap<deadline, index, AccountId> (mirrors Challenges/NextChallengeIndex).
    • New permissionless call: check_understake(provider).
    • on_initialize sweep extended to drain UnderstakeDeadlines the same
      bounded way it drains challenge timeouts.
    • New Error variant(s), e.g. ProviderNotUnderstaked.
  • crates/pallets/storage-provider/src/impls/challenges.rs: trigger
    check_understake from slash_provider_for_failed_challenge.
  • crates/pallets/storage-provider/src/benchmarking.rs: new benchmarks for
    the sweep and check_understake.
  • docs/design/scalable-web3-storage.md: Economic Model / Challenge Game
    section needs updating to document the post-slash grace period and
    auto-eviction, per this repo's design-doc-is-canonical rule.

Related

  • #310: qualitative description of the same underlying incentive gap.
  • Companion RFC: #386, exposure-based stake requirement. This RFC is its
    natural sequel.

cc @eskimor, since this also touches the Economic Model / Challenge Game
section of docs/design/scalable-web3-storage.md you authored.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in crates/pallets/storage-provider/src/lib.rs with ProviderInfo, deregister_at, and the existing bounded challenge-timeout sweep, then trace slash_provider_for_failed_challenge in impls/challenges.rs. The work is done when understake tracking, permissionless checking, deadline sweeping, and related benchmarks are implemented, and docs/design/scalable-web3-storage.md documents the post-slash grace period and auto-eviction.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.