Skip to content

fix: address non-blocking vesting review findings - #649

Open
n13 wants to merge 1 commit into
mainfrom
fix/vesting-review-polish
Open

fix: address non-blocking vesting review findings#649
n13 wants to merge 1 commit into
mainfrom
fix/vesting-review-polish

Conversation

@n13

@n13 n13 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #646. That PR merged with all blocking findings fixed; this one works through the non-blocking remainder. Each was evaluated on its own merits — several are deliberately not fixed, with the reasoning below.

Fixed

#12 + #14(a) — one shared cost model for ZK-tree leaf inserts. Depth-aware insert pricing was re-composed in four places (vesting weights, reversible-transfers weights, the proof-recorder extension, wormhole weights), and each composition called insert_leaf_db_ops() and insert_leaf_hash_ref_time() separately — two Depth trie reads per weight evaluation, on every get_dispatch_info. pallet_zk_tree::insert_leaf_weight_at_depth (and the Pallet::insert_leaf_weight live-depth wrapper) now composes DB ops, Poseidon path hashing and per-key PoV from one depth read, and the call sites use it. A future change to the cost model reaches every caller instead of three of four.

This also fixes an omission in the extension: per_transfer_weight charged the tree's DB ops and hashing but declared zero proof size, while vesting, reversible-transfers and wormhole all charge tree_reads × TREE_KEY_POV. Recorded transfers now declare the PoV their leaf insert actually consumes — pinned by a new assertion in per_transfer_weight_includes_tree_hash_compute. This is the one behavior-affecting change here: per-transfer PoV goes up, materially so at deep trees, which is the honest bound validators re-execute against.

#11 — weights augmentation is no longer silently breakable. payout_weight subtracts hand-maintained BENCHMARK_TREE_* counts out of the generated base and adds the live-depth insert back; a zk-tree change that made an insert cheaper than the benchmark-time ops would turn that into an under-charge with no compile error and no failing benchmark. Two tests now pin it: the augmented weight covers the benchmarked base at every reachable depth, and the depth-blind () impl bounds the live-depth impl at every depth. The constants also got the doc comment explaining where they come from.

#9 — mock state leaks between tests. ExistentialDeposit, TreasuryAccount, PayoutQuantum, MinimumPayout, MinClaimInterval and RECORDED_PROOFS all live in thread-local storage, and the test harness reuses worker threads. A test doing PayoutQuantum::set(3_000) leaked into whichever test ran next on the same worker; the suite passed by scheduling luck. The ext builder now resets every one of them to a named default. new_test_ext also computes the pot endowment from the default const rather than the live static, so there is no read-before-reset window.

#14(c) — claim and retarget_schedule duplicated the settle block (pay out, advance claimed, stamp last_claim_at). Extracted to Pallet::settle, so the two paths cannot drift on what a payout does to the schedule.

#13 (partial) — lazy pot derivation. The event scan derived the vesting pot account (a Blake2b hash) on every successful extrinsic, purely so the filter could skip pot events that the overwhelming majority of extrinsics never emit. Now derived on first Transfer event.

#10 + #14(b) — the two properties that keep payouts exitable are now machine-checked. Const assertions in the runtime: the vesting payout quantum (anchored to pallet_wormhole::SCALE_DOWN_FACTOR) equals pallet_zk_tree::tree::AMOUNT_SCALE_DOWN_FACTOR, the value that actually decides whether a leaf is non-zero; and MAX_SUPPLY sits below the leaf's u32 amount ceiling, which is why no single payout can hit the clamp (~42.9M QUAN clamp vs a 21M QUAN cap). No cap on total was added — total issuance already bounds it, and now the compiler enforces that it keeps doing so.

#14(d) (partial) — deduplication. MAX_SUPPLY and MILLIS_PER_DAY are single constants in runtime/src/lib.rs instead of magic numbers repeated across configs and genesis_config_presets; governance/vesting.rs uses TestCommons::account_id instead of its own copy.

#5 — documented, not fixed (see below), with the rationale recorded next to count_transfers and in RUNTIME_SURFACE.md.

Deliberately not fixed

#6total == MinimumPayout schedules cannot be ended early. This is the design, not a defect. A 1-QUAN schedule vests to its beneficiary on its own schedule; the treasury waiting it out is the intended behavior for the smallest valid grant. Loosening the guard to PayoutQuantum would let end_schedule emit payouts that claim rejects, which is a worse inconsistency than the one it removes.

#5count_transfers overcharges plain transfers into the pot. Resolving the destination in count_transfers means a Lookup on the hottest call in the runtime, to spare a handful of one-off ED-bootstrap transfers. The overcharge direction is conservative. Documented in place rather than fixed.

#13 (partial) — EnsureTreasury re-reading treasury_account(). The suggestion was to take the account from EitherOfDiverse::Success, but that Success is Either<(), AccountId>: the Root arm carries no account, so the pallet still has to read the storage key. No saving available without dropping Root as break-glass admin.

#14(d) (partial) — MockProofRecorder copies. Four crates have one (multisig's is a no-op stub, mining-rewards and reversible-transfers share a four-accessor API, vesting's is tuple-based), over different account and balance types. Consolidating means a declarative macro in qp-wormhole plus dev-dependency wiring in four crates — a cross-cutting test-infrastructure refactor rather than a vesting follow-up. Worth doing; not here.

#15 — the pot-skip hardcodes a per-pallet exemption in runtime-wide transaction infrastructure. Real design debt, but both suggested remedies are worse today: eventless increase_balance cannot express a payout (the pot must be debited, not just the beneficiary credited), and a recorder-side registry is over-engineering for a single pallet. Left as the documented, commented special case.

#7 and #8 were already fixed before merge in 4f584733.

Verification

cargo test --workspace: 2166 passed, 0 failed — including 59 vesting pallet tests (63 with runtime-benchmarks), 37 runtime lib tests and 34 integration tests. cargo clippy --locked --workspace clean. runtime-benchmarks and try-runtime feature builds check. Formatted with cargo +nightly fmt and taplo.

Weights were not regenerated: the generated benchmark file is untouched, and only the augmentation around it changed.

spec_version 142 → 143, since runtime weights change.

Follow-up to #646, covering the polish items left open by the review.

- One shared cost model for ZK-tree leaf inserts. `insert_leaf_weight[_at_depth]`
  in pallet-zk-tree composes the depth-scaled DB ops, the Poseidon path hashing
  and the per-key PoV, from a single `Depth` read. Vesting, reversible-transfers
  and the proof-recorder extension now price inserts through it instead of
  re-composing the parts (each of which read `Depth` again). This also gives the
  extension the tree PoV it was omitting.
- Pin the vesting weights augmentation: it subtracts hand-maintained benchmark
  tree-op counts from the generated base, and a zk-tree cost-model change could
  silently make that an under-charge. Tests assert the augmented weight covers
  the benchmarked base at every depth, and that the `()` impl bounds all of them.
- Reset the vesting mock's `static` config and recorded proofs per test. The
  harness reuses worker threads, so a `PayoutQuantum::set` leaked into whichever
  test ran next on the same worker.
- Extract `settle`: `claim` and `retarget_schedule` duplicated the payout,
  `claimed` and `last_claim_at` updates.
- Derive the vesting pot lazily in the event scan (it costs a Blake2b hash on
  every extrinsic, and most emit no `Transfer` at all).
- Const-assert the two properties that keep payouts exitable: the vesting quantum
  equals the tree's leaf amount scale factor, and max supply stays below the
  leaf's u32 amount ceiling.
- Deduplicate `MAX_SUPPLY`, `MILLIS_PER_DAY` and the integration tests' account
  helper; document the accepted inbound-pot weight overcharge.

spec_version 142 -> 143.
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