paritytech / paritytech/web3-storage
RFC: Forced re-stake deadline + auto-eviction after a slash
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_extensionsare 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).
- On slash, if the provider's stake now falls below
required_stakefor its
current book, set a new field,understake_since: Option<BlockNumberFor<T>>, and flipaccepting_primary/
accepting_extensionstofalseimmediately (same asderegister_provider
already does), so at minimum no new exposure can accumulate on top of an
already-broken guarantee. - Start a bounded deadline:
understake_deadline = now + T::DeregisterAnnouncementPeriod. - If the provider calls
add_stakebefore the deadline and clears
required_stakeagain, clearunderstake_sinceand restore acceptance
flags to their prior values. - If the deadline passes without sufficient stake, a bounded per-block sweep
(mirroring the existing challenge-timeout sweep) evicts remaining
agreements: effectively an automatic, bulkremove_slashedacross every
bucket that provider still holds, refunding eachpayment_lockedto its
owner instead of requiring each bystander to find out and act
individually.
Design decisions
- Grace period: reuse
DeregisterAnnouncementPerioddirectly, no new
config. Rather than introduce a separate tunable, reuse the existing
constant. It already satisfiesDeregisterAnnouncementPeriod > ChallengeTimeoutvia 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. - 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/LastSweptChallengeBlockalready are. No new sweep
architecture, just the same pattern applied to a second deadline type. - Trigger generically, not just from the slash path. A single
check_understake(provider)internal function, called deterministically
fromslash_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 fieldunderstake_since: Option<BlockNumberFor<T>>.- New storage:
UnderstakeDeadlines: StorageDoubleMap<deadline, index, AccountId>(mirrorsChallenges/NextChallengeIndex). - New permissionless call:
check_understake(provider). on_initializesweep extended to drainUnderstakeDeadlinesthe same
bounded way it drains challenge timeouts.- New
Errorvariant(s), e.g.ProviderNotUnderstaked.
crates/pallets/storage-provider/src/impls/challenges.rs: trigger
check_understakefromslash_provider_for_failed_challenge.crates/pallets/storage-provider/src/benchmarking.rs: new benchmarks for
the sweep andcheck_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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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