Skip to content

Collateral-floor sweep: drive vote_deactivate after a min_collateral raise - #616

Open
anderdc wants to merge 2 commits into
feat/crown-rate-bandfrom
feat/floor-sweep
Open

Collateral-floor sweep: drive vote_deactivate after a min_collateral raise#616
anderdc wants to merge 2 commits into
feat/crown-rate-bandfrom
feat/floor-sweep

Conversation

@anderdc

@anderdc anderdc commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What

Stacked on #614. The precondition for raising min_collateral (planned 0.5–1 SOL): without this, an admin floor raise strands every under-floor miner as a permanent zombie — set_min_collateral doesn't touch MinerState, open_or_request refuses to reserve them, so the fee/slash apply_penalty path that auto-deactivates under-floor miners can never fire. They stay active indefinitely, unreservable by takers but still earning crown. The contract's vote_deactivate quorum instruction handles exactly this case; it just had no driver — no discriminator in layouts.py, no client method, no validator loop.

Cost model (the "be smart about RPC" part)

  • Steady state: zero added RPC. The sweep reads min_collateral off the existing SolanaConfigCache (one Config read per 300s already paid for by swap bounds) and compares it to the last-seen value — one int comparison per forward step.
  • The getProgramAccounts scan runs only when the cached floor rises, or once on the first step after boot (covers a raise that happened while this validator was offline). Floor decreases don't rescan.
  • Stragglers get per-miner account reads on a 300s retry cadence, only while the pending set is non-empty: busy miners (the contract rejects kicking a miner with an in-flight swap / unexpired busy_until — they retry once idle; they can't take new swaps, so everyone converges within one fulfillment timeout), and cast votes awaiting quorum (a miner leaves pending only when the chain shows active = false or collateral ≥ floor, so a restart or reset round can't strand a half-kicked miner).
  • has_voted gate prevents duplicate votes; a miner that tops up instead of getting kicked is released.

Pieces

  • layouts.py: vote_deactivate discriminator (sha256(global:vote_deactivate)[:8], derivation verified against the known deactivate discriminator).
  • client.py: vote_deactivate(miner) mirroring vote_activate with REQ_DEACTIVATE.
  • bounds_cache.py: min_collateral() accessor on the existing TTL cache.
  • floor_sweep.py: the sweep + a never-raises maybe_sweep_floor forward hook (same containment pattern as maybe_vote_weights). Respects watch mode (WOULD vote_deactivate …).
  • 8 new tests: steady-state costs nothing, raise kicks under-floor active idle miners only, busy-retry, no double-vote while awaiting quorum, top-up release, watch mode.

Deploy order

Validators must run this before the alw admin set-min-collateral raise fires — sequence: merge → deploy to ≥67% of validator stake → raise the floor. The under-floor zombie window is then bounded by vote latency (minutes), not forever.

Full suite: 856 passed.

🤖 Generated with Claude Code

anderdc and others added 2 commits August 4, 2026 09:35
set_min_collateral only rewrites the Config: an active miner left under
the new floor can no longer be reserved (open_or_request gate), so the
fee/slash paths that auto-deactivate under-floor miners never fire and
they stay active forever — unreservable by takers yet still earning
crown. The contract's vote_deactivate quorum path was built for exactly
this, but nothing drove it.

CollateralFloorSweep watches the TTL-cached Config each forward step —
steady state is a single integer comparison, no RPC. Only when the
cached floor rises (or once on boot, covering a raise made while the
validator was offline) does it scan MinerState accounts and vote to
deactivate active idle miners below the floor. Busy miners and votes
short of quorum stay pending and are rechecked with per-miner reads on
a slow cadence until the set drains. Watch mode logs WOULD-votes.

Deploy this to validators before raising min_collateral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scan obligation is now an armed flag set on boot and on each raise
edge, cleared only by a completed scan: an RPC failure during the arm
scan retries on the slow cadence instead of silently dropping the one
event the sweep exists for. The retry cursor advances before RPC calls
so failures back off rather than re-firing every forward step, a fresh
raise resets the cursor so it never waits out a stale back-off, and one
unreadable pending miner skips instead of aborting the recheck pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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