Skip to content

Feature/blk 1005 delegator restaking#388

Open
sfffaaa wants to merge 7 commits into
devfrom
feature/blk-1005-delegator-restaking
Open

Feature/blk 1005 delegator restaking#388
sfffaaa wants to merge 7 commits into
devfrom
feature/blk-1005-delegator-restaking

Conversation

@sfffaaa

@sfffaaa sfffaaa commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Delegator staking rewards are restaked in place — added to the delegator's balance
lock, its delegation to that collator, and the collator's total — instead of paid out
as liquid balance. Every delegator auto-compounds with no keeper transaction and no
user action; a delegator wanting cash calls delegator_stake_less (14-day unbonding
on peaq, 7 on krest).

What changed

payout_collator is reworked into a two-phase (preflight → commit) payout per delegator reward:

  • Phase 1 runs all fallible checks (delegator still delegates this collator, STAKING_ID
    lock exists, no overflow); Phase 2 does only guaranteed-success writes. Required because
    on_finalize is Mandatory class and cannot revert — "pay then fail midway" is split-brain.
  • Any preflight failure → plain-payout fallback.
  • Per-collator shared writes are batched (one update_top_candidates + CandidatePool /
    TotalCollatorStake write), keeping the loop O(m·log m).

Also:

  • New increase_lock_from_reward — adds the reward to the lock without touching pending
    Unstaking (reusing increase_lock would silently consume queued withdrawals; sole
    divergence from delegator_stake_more).
  • Two events: DelegatorRewardRestaked (compounded) / DelegatorRewardPaidNotRestaked (fallback).
  • try_state invariants (the pallet had none): lock == total + Σ Unstaking,
    candidate.total == Σ stakes + self, TotalCollatorStake == Σ selected.
  • Conditional weight reserve in on_initialize — reserve the heavy payout weight only when a
    payout is pending (DelayedPayoutInfo::exists()), so idle blocks keep their capacity.
  • Fresh benchmarks for payout_collator and prepare_delayed_rewards (delegator component to
    100); fixed two pre-existing stale benchmarks that broke when round rotation moved to Session.
  • AUTO_RESTAKE.md — indexer/wallet migration note + user withdrawal note.

Design decisions

  • Inline restake over a deferred queue + holding pot: the queue only defers work to later
    blocks, but per-block PoV/weight has large headroom, so deferral is unnecessary; inline
    reuses existing code with zero new storage.
  • Mandatory for all (no opt-in flag): delegator_stake_less is the opt-out.
  • Collator's own reward is not restaked (scope: delegators only; stake-share drift accepted).

Test plan

  • 78 pallet unit tests pass (restake happy path, pending-Unstaking invariant, fallback
    paths, zero-reward skip, batch at 100, compounding, session-boundary ordering,
    try_state, idempotency, 0/1-delegator boundary, EVM-precompile contract).
  • Benchmark gate: payout_collator at m=100 → proof_size 377 kB (< 500 kB), ref_time
    18.9 ms (< 100 ms).
  • Live 64×100 at the mainnet ceiling (MaxCollatorCandidates=64 × MaxDelegatorsPerCollator=100)
    on a local parachain, on both the peaq-dev and peaq mainnet runtimes: 6400 delegators
    restaked, 0 failures, verified block-by-block; the heavy snapshot block never stalls
    (real PoV ~410 kB = 8% of the 5 MiB budget).

Note

These weights are machine-generated by the frame-benchmarking CLI (peaq-node benchmark pallet --chain dev-local --pallet parachain_staking --extrinsic payout_collator --steps 20 --repeat 5, same for prepare_delayed_rewards), not hand-tuned. It runs each call ~100 times across a sweep of the delegator/collator counts and linear-regresses to the intercept + slope·n coefficients, so from_parts(ref_time, proof_size) is measured compute-picoseconds plus worst-case PoV bytes, with the base taken as the regression intercept — a safe upper bound above the fastest run. At the 64×100 mainnet ceiling that's ≈18.9 ms / ≈377 KiB PoV for payout_collator, and the snapshot's PoV scales with collator count only, so it stays well within the block PoV budget.

jaypan added 3 commits July 3, 2026 18:13
Delegator staking rewards are restaked automatically at payout time instead of landing in free balance; each reward is added to the delegator's bonded stake and their collator. Collator rewards stay liquid. Two-phase (preflight -> transfer -> infallible commit) payout in on_finalize; increase_lock_from_reward leaves pending Unstaking untouched; all delegators of a collator share one candidate (batch-flushed once). on_initialize conditionally reserves the payout weight; do_try_state asserts totals + the STAKING_ID lock invariant (incl. fully-exited accounts). New DelegatorRewardRestaked / DelegatorRewardPaidNotRestaked events; reward for round R restakes in R+1, compounds from R+2.

Design: inline restake at payout over a deferred queue -- no new storage/migration/keeper; per-block delegator count already bounded by MaxDelegatorsPerCollator.
…e benchmarks

Add payout_collator (1 collator x n delegators = the per-block payout worst case) and prepare_delayed_rewards (n collators x m delegators = the session-boundary snapshot block, the heaviest single block in the flow) benchmarks, so both heavy blocks are measurable for weight/PoV. weights.rs keeps a conservative placeholder for payout_collator until the CLI numbers are generated.

Fix two pre-existing benchmarks broken by round rotation moving to pallet_session: remove on_initialize_round_update, repair execute_leave_candidates (bump Round.current directly).
@sfffaaa sfffaaa requested a review from neutrinoks July 8, 2026 08:45
jaypan added 3 commits July 8, 2026 10:48
…nd snapshot

Replace the hand-estimated payout_collator weight and the manual
reads_writes plumbing in prepare_delayed_rewards with the numbers
generated by the frame-benchmarking CLI (delegator component measured
to 100).

- weightinfo.rs: declare prepare_delayed_rewards(n, m) in the trait
- weights.rs: benchmarked payout_collator, add prepare_delayed_rewards
- lib.rs: prepare_delayed_rewards now returns
  WeightInfo::prepare_delayed_rewards(n, m); the manual read/write
  tally (and the helper weight returns it summed) are dropped in favor
  of the measured weight.
@sfffaaa sfffaaa closed this Jul 8, 2026
@sfffaaa sfffaaa reopened this Jul 8, 2026
@peaq-coolify

peaq-coolify Bot commented Jul 8, 2026

Copy link
Copy Markdown

The preview deployment for peaqnetwork/peaq-network-node:ci/012453_add-auto-upgrade-on-preview-environment-iswg08ckww0c4k48oggoo400 is ready. 🟢

Open Preview | Open Build Logs

Last updated at: 2026-07-08 09:40:51 CET

… observable

The reward payout runs in on_finalize (Mandatory): it must neither panic
nor silently lose funds. Harden the path:

- Log every failed pot -> account transfer (restake, plain-payout fallback,
  and collator reward) instead of skipping the reward with no signal. The
  failure is still handled (skip, never propagate); it is now visible.
- Replace the expect() in get_delgators_reward_per_session with truncate_from:
  the vec can never exceed MaxDelegatorsPerCollator (1:1 map over a bounded
  snapshot), but truncate_from cannot panic in the Mandatory hook and logs the
  provably-impossible overflow.
- Use saturating add/sub in the reward math so arithmetic can never panic on
  overflow (values are bounded by issuance; belt-and-braces).

No happy-path behavior change; 78 pallet tests still pass.
@peaq-coolify

peaq-coolify Bot commented Jul 9, 2026

Copy link
Copy Markdown

The preview deployment for peaqnetwork/peaq-network-node:ci/012453_add-auto-upgrade-on-preview-environment-iswg08ckww0c4k48oggoo400 is ready. 🟢

Open Preview | Open Build Logs

Last updated at: 2026-07-09 10:21:44 CET

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