Skip to content

fix - BZE-142 - hooks reach the msg servers, panic-safe distribution queues, DR genesis value checks - #113

Merged
busydonna merged 5 commits into
mainfrom
fix/BZE-142-review-hardening
Sep 17, 2026
Merged

busydonna merged 5 commits into
mainfrom
fix/BZE-142-review-hardening

Conversation

@busydonna

@busydonna busydonna commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Four independent hardening items that came out of the v8.2.0 release review (BZE-142): three in x/rewards plus the same msg-server wiring fix in x/tradebin. None changes on-chain behaviour under current mainnet conditions; they close latent defects. Each item is its own commit so it can be cherry-picked into v8.2.0 or deferred on its own:

  1. Hooks reach the msg server — the msg server held a by-value keeper copy taken at RegisterServices (inside appBuilder.Build), before app.go wires any hooks, so Keeper.SetHooks could never reach JoinStaking / ExitStaking / DeleteStakingReward. Not consensus-affecting: no module registers these hooks today.
  2. Panic-safe EndBlock distribution queues — the staking-reward and denom-reward-schedule payouts now run per entry inside bzeutils.ApplyFuncIfNoError (cache context + recover), like the unlock and trading-reward queues. Behaviour differs only on the panic path (skip and log instead of halting every node), so it is listed under State Machine Breaking and must activate with the upgrade handler.
  3. Value-level Denom Rewards genesis validation — validate-genesis now rejects duplicates, negative stakes/accumulators, participant amounts that do not sum to the pool's stake, indexes ahead of their accumulator, and zombie schedules. Only genesis validation changes; every chain export still passes (the existing keeper round-trip tests are the guard).
  4. Same wiring fix for tradebin — the tradebin msg server also held a by-value keeper copy, so the AMM swap path never invoked the order-fill hooks (the orderbook path, run from EndBlock off the keeper pointer, did). Not consensus-affecting with the current hook: it resolves trading rewards by orderbook market id (base/quote) while AMM swaps report the pool id (base_quote), so the lookup misses either way.

Changes

Commit 1 — share the keeper pointer with the rewards msg server

  • x/rewards/keeper/msg_server.go: msgServer embeds *Keeper; NewMsgServerImpl(k *Keeper).
  • x/rewards/module/module.go: RegisterServices passes am.keeper (pointer); corrected the comment that claimed the pointer already made hooks visible.
  • x/rewards/keeper/*_test.go: call sites pass the keeper pointer; registerHooks no longer rebuilds the msg server after SetHooks.
  • x/rewards/module/hooks_wiring_test.go (new): registers services through the real SDK configurator, calls SetHooks afterwards (the app's order) and dispatches MsgJoinStaking through the msg service router; the hook must fire. Fails on the previous wiring.
  • x/rewards/module/migration_wiring_test.go: fixture keeper is a pointer and manager() also returns the msg service router.
  • CHANGELOG.md: Bug Fixes entry.

Commit 2 — recover from panics in the EndBlock distribution queues

  • x/rewards/keeper/service_denom_reward.go: safeDistributeDenomRewardSchedule wraps each schedule's pass; a panic is logged with the schedule id and the cursor keeps moving.
  • x/rewards/keeper/service_staking_reward.go: same treatment for ProcessStakingRewardsDistributionQueue (safeDistributeStakingReward).
  • x/rewards/keeper/service_distribution_panic_safety_test.go (new): one entry of each queue panics (epoch mock for DR, a genuine LegacyDec overflow for SR); the other entries are paid, a marker written before the panic is rolled back, the queue drains, and the next day pays the skipped entry.
  • x/rewards/keeper/service_denom_reward_distribution*_test.go: epoch mock matches any context, since the read now happens on the cache context.
  • CHANGELOG.md: State Machine Breaking entry.

Commit 3 — validate denom reward genesis values, not only references

  • x/rewards/types/genesis.go: validateDenomRewards adds duplicate detection for prizes / participants / indexes / schedules, non-negative staked amounts and accumulators, positive participant amounts summing to the pool's staked amount, indexes never ahead of the accumulator, schedules pointing at an existing prize with a positive daily amount, non-zero duration and payouts < duration. Absent JSON numeric fields still count as zero. Deliberately not enforced: the prize-denom cap (governance may lower the param below an existing pool's count, so a real export can exceed it) and indexes without a participant record.
  • x/rewards/types/genesis_test.go: one table case per rule plus three positive cases (nil numerics, prizes above the cap, index without participant); 35 cases total.
  • x/rewards/keeper/genesis_absent_fields_test.go: the hand-edited genesis with absent amounts is now rejected by validation; the test still pins the panic-free import path.
  • CHANGELOG.md: Improvements entry.

Commit 4 — share the keeper pointer with the tradebin msg server

  • x/tradebin/keeper/msg_server.go: msgServer embeds *Keeper; NewMsgServerImpl(k *Keeper).
  • x/tradebin/module/module.go: RegisterServices passes am.keeper (pointer).
  • x/tradebin/keeper/keeper_test.go: suite passes the keeper pointer.
  • x/tradebin/module/hooks_wiring_test.go (new): three tests through the real SDK configurator and msg service router — hooks registered after RegisterServices (the app's order) fire on a MsgMultiSwap; hooks registered before (the pre-v8 order) fire too; the msg server sees hooks registered after it was built. The first and the last fail on the previous wiring.
  • CHANGELOG.md: Bug Fixes entry.

Verification

  • ./scripts/testing/test-package.sh (the CI set, -race) green.
  • go build ./... and go vet ./x/rewards/... clean.

RegisterServices runs inside appBuilder.Build, before app.go wires any
hooks, so the by-value keeper copy embedded in the msg server froze the
nil hooks at registration time and Keeper.SetHooks could never reach
JoinStaking, ExitStaking or DeleteStakingReward. Embed *Keeper instead
and pass the module's pointer; tests no longer rebuild the msg server
after SetHooks. A module-level test dispatches MsgJoinStaking through the
real msg service router with hooks registered after RegisterServices.
ProcessStakingRewardsDistributionQueue and ProcessDenomRewardsDistributionQueue
ran every per-entry payout unguarded, so a corrupt record or an accumulator
overflow would have halted the chain. Each entry now runs through
bzeutils.ApplyFuncIfNoError (cache context + recover), matching the unlock and
trading-reward queues: the bad entry is logged and skipped with its writes
discarded, the rest of the batch is paid and the queue keeps draining.

Tests inject a panic into one entry of each queue (epoch mock for DR, a
LegacyDec overflow for SR) and assert the others are paid, the marker write
made before the panic is rolled back and the queue drains. Existing queue
tests match the epoch read on any context since it now runs on the cache one.
validateDenomRewards accepted any numeric value as long as the references
resolved: negative stakes and accumulators, participant amounts that do not
sum to the pool's staked amount, indexes ahead of their prize accumulator,
zero-amount or zero-duration schedules and schedules with payouts >= duration
(re-enqueued every day, never finishing), and duplicate records (last-wins on
import). None can come from an export; all can come from a hand-edited genesis
and some strand escrow or wedge the daily pass. Reject them in validate-genesis.

Deliberately not enforced: the prize-denom cap (governance may lower the
param below an existing pool's count, so a real export can exceed it) and
indexes without a participant record. Absent JSON numeric fields still count
as zero; the absent-fields keeper test now expects the rejection while still
pinning the panic-free import path.
@busydonna
busydonna marked this pull request as ready for review September 11, 2026 13:44
Same defect as the rewards msg server: RegisterServices (inside
appBuilder.Build) copied the keeper by value before app.go registered the
order-fill hooks, so the AMM swap path (MsgMultiSwap -> onSwapSuccess)
read an empty hook slice forever while the orderbook path, whose
ProcessingEngine is built from the keeper pointer each block, saw them.
Embed *Keeper and pass the module's pointer.

No on-chain change with the current hook: the rewards hook resolves
trading rewards by orderbook market id (base/quote) and AMM swaps report
the pool id (base_quote), so the lookup misses either way. Module-level
tests dispatch a MsgMultiSwap through the real msg service router with
hooks registered after RegisterServices (the app's order), before it
(the v7 order), and check the msg server shares the keeper; the first
and last fail on the previous wiring.
@busydonna busydonna changed the title fix - BZE-142 - hooks reach the rewards msg server, panic-safe distribution queues, DR genesis value checks fix - BZE-142 - hooks reach the msg servers, panic-safe distribution queues, DR genesis value checks Sep 11, 2026
@busydonna
busydonna marked this pull request as draft September 11, 2026 13:48
@busydonna
busydonna marked this pull request as ready for review September 17, 2026 21:37
@busydonna
busydonna merged commit 780ab35 into main Sep 17, 2026
1 check passed
@busydonna
busydonna deleted the fix/BZE-142-review-hardening branch September 17, 2026 21:44
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