Skip to content

chore: distribute LP rewards after market settle#1309

Merged
MicBun merged 9 commits into
mainfrom
distributeLpRewards
Feb 2, 2026
Merged

chore: distribute LP rewards after market settle#1309
MicBun merged 9 commits into
mainfrom
distributeLpRewards

Conversation

@MicBun
Copy link
Copy Markdown
Contributor

@MicBun MicBun commented Feb 2, 2026

resolves: https://github.com/truflation/website/issues/3256

Summary by CodeRabbit

  • New Features
    • LP rewards extension added and registered; leadership-aware periodic sampling with signer/broadcaster support and per-market sampling/broadcast flow.
  • Database
    • New lp_rewards_config table and active-market query with sensible defaults; settlement max_markets_per_run increased 10 → 100.
  • Bug Fixes
    • Collateral validation relaxed to vault_balance >= expected_collateral.
    • Prevent overlapping settlement runs via processing guard and add nonce-aware retry for settlement broadcasts.
  • Tests
    • End-to-end LP rewards tests added; settlement tests updated for full-payout expectations.

@MicBun MicBun self-assigned this Feb 2, 2026
@holdex
Copy link
Copy Markdown

holdex Bot commented Feb 2, 2026

Time Submission Status

Member Status Time Action Last Update
MicBun ✅ Submitted 8h Update time Feb 2, 2026, 2:32 PM

You can submit time with the command. Example:

@holdex pr submit-time 15m

See available commands to help comply with our Guidelines.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

Warning

Rate limit exceeded

@MicBun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a new tn_lp_rewards extension (scheduling and broadcasting sample_lp_rewards) with engine helper ops and tests; introduces DB migration and actions for lp_rewards_config; updates settlement and migration SQL (collateral check, settlement defaults, payout semantics); prevents overlapping settlement runs via an atomic processing guard and adds nonce-aware retries in settlement broadcast paths.

Changes

Cohort / File(s) Summary
Extension registration & core
extensions/register.go, extensions/tn_lp_rewards/tn_lp_rewards.go
Register tn_lp_rewards extension; implement Extension singleton, lifecycle/leadership hooks, config reload, signer/broadcaster wiring, TxBroadcaster adapter, URL normalization, and sampling orchestration/scheduling.
Engine helper ops
extensions/tn_lp_rewards/internal/engine_ops.go
Add EngineOperations type with fresh-read tx support, LoadLPRewardsConfig, GetActiveMarkets, and BroadcastSampleLPRewards (account lookup, nonce handling, tx build/sign/broadcast, error handling).
Migrations
internal/migrations/037-order-book-validation.sql, internal/migrations/041-settlement-config-actions.sql, internal/migrations/042-lp-rewards-config.sql
037: change vault collateral check =>=. 041: bump default max_markets_per_run (10→100) and adjust role/member lookup identifiers. 042: add lp_rewards_config table, defaults, and public actions get_lp_rewards_config, update_lp_rewards_config, get_active_markets_for_sampling.
Settlement processing & scheduler
extensions/tn_settlement/extension.go, extensions/tn_settlement/internal/engine_ops.go, extensions/tn_settlement/scheduler/..., extensions/tn_settlement/scheduler_lifecycle.go
Add isProcessing atomic flag and ProcessingGuard interface; expose TryStartProcessing/SetProcessing/IsProcessing; integrate guard into scheduler and lifecycle to prevent overlapping runs; add nonce-aware retry logic and fresh-nonce broadcast helpers in settlement engine ops.
Order-book settlement logic
internal/migrations/033-order-book-settlement.sql
Rewrite settlement payout to remove redemption-fee flow; winners receive full payout (zero-sum settlement); remove settlement-time fee accounting and distribution.
Tests
tests/streams/order_book/lp_rewards_config_test.go, extensions/tn_settlement/settlement_integration_test.go, tests/streams/order_book/settlement_payout_test.go, tests/streams/order_book/split_limit_order_test.go
Add E2E tests for lp_rewards_config and extension config loading; update settlement tests to expect full payouts and new defaults; adjust test comments to match new settlement behavior.
Misc
.gitignore
Minor gitignore entry adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Ext as "tn_lp_rewards Extension"
    participant EngOps as "EngineOperations"
    participant DB as "Database"
    participant Accts as "Accounts"
    participant RPC as "JSON-RPC Broadcaster"
    participant Engine as "Engine/Service"

    Ext->>EngOps: reloadConfig(ctx)
    EngOps->>DB: LoadLPRewardsConfig()
    DB-->>EngOps: (enabled, interval, maxMarkets)
    EngOps-->>Ext: updated config

    Ext->>EngOps: sampleLPRewards(ctx, blockHeight)
    EngOps->>DB: GetActiveMarkets(limit)
    DB-->>EngOps: [marketIDs]

    loop per market id
        EngOps->>Engine: resolve chain/market metadata
        Engine-->>EngOps: chainID
        EngOps->>Accts: fetch signer account & next nonce
        Accts-->>EngOps: account + nonce
        EngOps->>EngOps: build & sign sample_lp_rewards tx
        EngOps->>RPC: BroadcastTx(signedTx, sync)
        RPC-->>EngOps: txHash, txResult
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

type: feat

Suggested reviewers

  • pr-time-tracker

Poem

🐰 I hopped through migrations, counting markets bright,
I signed a few nonces deep into the night.
I sampled rewards, one tx at a time,
With carrots and logs and a rhythm in rhyme.
✨🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: distribute LP rewards after market settle' directly aligns with the PR's main objective of implementing LP rewards distribution post-settlement, as evidenced by the new tn_lp_rewards extension and supporting migrations.
Docstring Coverage ✅ Passed Docstring coverage is 93.10% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch distributeLpRewards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@extensions/tn_lp_rewards/internal/engine_ops.go`:
- Around line 164-254: The BroadcastSampleLPRewards function currently reads a
fresh nonce from the DB each call (nextNonce) which causes nonce reuse in async
batch calls; change the signature of BroadcastSampleLPRewards to accept an
explicit caller-supplied nonce (e.g. add parameter callerNonce uint64) and use
that when non-zero, otherwise fall back to the existing DB lookup logic; replace
usages of nextNonce in CreateNodeTransaction and the debug log with the resolved
nonce variable (e.g. nonce := callerNonce; if nonce == 0 { ... existing DB logic
sets nonce }) and update all call sites to pass a monotonically increasing nonce
when batching; ensure the function still returns errors as before.
- Around line 44-55: The getFreshReadTx method on EngineOperations currently
falls back to returning e.db without checking for nil; update
EngineOperations.getFreshReadTx to check if e.dbPool is nil and then verify e.db
is non-nil before returning — if e.db is nil return a non-nil error (and a no-op
cleanup func) so callers like ExecuteWithoutEngineCtx don't proceed with a nil
DB; keep the existing behavior when e.db is present and preserve the readTx
cleanup closure for e.dbPool paths.

In `@extensions/tn_lp_rewards/tn_lp_rewards.go`:
- Around line 381-389: The loop calling BroadcastSampleLPRewards re-reads the
account nonce for each market causing nonce reuse because broadcasts are async;
fix by reading the current nonce once before the loop and deterministically
assigning unique nonces per tx (e.g., call your existing nonce-read routine
once, store as baseNonce, then for each market pass baseNonce+i into
BroadcastSampleLPRewards or add a nonce parameter to BroadcastSampleLPRewards so
it no longer fetches the nonce internally), or replace the per-call DB read with
a proper nonce manager that atomically reserves/increments nonces; update the
call sites (the for loop over markets using ext.engOps.BroadcastSampleLPRewards
and the BroadcastSampleLPRewards signature/implementation) to use the
single-read-and-increment approach.
- Around line 294-325: leaderEndBlock reads extension config fields without
locking while reloadConfig and sampleLPRewards run concurrently, causing races;
fix by taking ext.mu.RLock in leaderEndBlock, snapshot the needed fields
(enabled, samplingIntervalBlocks, configReloadInterval, maxMarketsPerRun) into
local variables, release the RLock, and then use those locals for the checks and
decisions (including atomic handling of lastCheckedHeight). Do not access
ext.enabled or ext.samplingIntervalBlocks after releasing the lock; instead pass
any needed snapshot like maxMarketsPerRun as an argument to sampleLPRewards (or
have sampleLPRewards accept a config struct) so sampleLPRewards does not read
the field unprotected. Ensure reloadConfig continues to use ext.mu for writes.

In `@tests/streams/order_book/lp_rewards_config_test.go`:
- Around line 64-68: The anonymous row handler currently uses unsafe type
assertions row.Values[0].(bool), row.Values[1].(int64), row.Values[2].(int64)
which can panic depending on backend; replace these with guarded type switches
like the pattern already used for max_markets_per_run so that enabled,
samplingInterval and maxMarkets are assigned by switching on both int and int64
(and handle unexpected types), and apply the same defensive switch-based casting
to the other instances where those columns are read (the other test handlers
that set enabled, samplingInterval, maxMarkets).
🧹 Nitpick comments (3)
extensions/tn_lp_rewards/tn_lp_rewards.go (1)

126-174: Surface config-load errors instead of ignoring.
LoadLPRewardsConfig errors are currently dropped, which hides DB/migration issues. Logging once at startup would aid diagnostics.

Suggested tweak
-    enabled, interval, maxMarkets, _ := engOps.LoadLPRewardsConfig(ctx)
+    enabled, interval, maxMarkets, err := engOps.LoadLPRewardsConfig(ctx)
+    if err != nil {
+        logger.Warn("failed to load LP rewards config; using defaults", "error", err)
+    }
internal/migrations/042-lp-rewards-config.sql (2)

83-119: Add explicit input guards and a reasonable upper bound.
enabled can be NULL and will hit a NOT NULL constraint with a generic error, and max_markets_per_run is unbounded. Consider explicit checks (e.g., 1–100) for clearer errors and safer limits.

Suggested validation
     if $has_role = FALSE {
         ERROR('Only network_writer can update LP rewards config');
     }
 
     -- Validate inputs
+    if $enabled IS NULL {
+        ERROR('enabled cannot be NULL');
+    }
     if $sampling_interval_blocks < 1 {
         ERROR('sampling_interval_blocks must be >= 1');
     }
-    if $max_markets_per_run < 1 {
-        ERROR('max_markets_per_run must be >= 1');
+    if $max_markets_per_run < 1 OR $max_markets_per_run > 100 {
+        ERROR('max_markets_per_run must be between 1 and 100');
     }

130-138: Validate $limit to avoid negative or unbounded queries.
A simple guard prevents accidental full-table scans or unexpected behavior.

Suggested guard
 CREATE OR REPLACE ACTION get_active_markets_for_sampling($limit INT)
 PUBLIC VIEW RETURNS (query_id INT) {
+    if $limit IS NULL OR $limit < 1 {
+        ERROR('limit must be >= 1');
+    }
     for $row in SELECT id FROM ob_queries
         WHERE settled = FALSE
         ORDER BY id ASC
         LIMIT $limit

Comment thread extensions/tn_lp_rewards/internal/engine_ops.go
Comment thread extensions/tn_lp_rewards/tn_lp_rewards.go
Comment thread extensions/tn_lp_rewards/tn_lp_rewards.go
Comment thread tests/streams/order_book/lp_rewards_config_test.go
@MicBun MicBun changed the title chore: distribute LP rewards evenly after market settle chore: distribute LP rewards after market settle Feb 2, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@extensions/tn_lp_rewards/internal/engine_ops.go`:
- Around line 168-259: The code in BroadcastSampleLPRewards can pass a nil e.db
when e.dbPool is nil; replace the manual dbPool check with the existing helper
getFreshReadTx to obtain a validated read transaction (and ensure it is closed
with defer readTx.Rollback(ctx)), then call e.accounts.GetAccount(ctx, readTx,
signerAccountID) instead of passing e.db; keep the same not-found handling to
set nextNonce and preserve existing error returns and logging.

Comment thread extensions/tn_lp_rewards/internal/engine_ops.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@extensions/tn_lp_rewards/tn_lp_rewards.go`:
- Around line 141-155: The code assigns enabled/interval/maxMarkets even when
engOps.LoadLPRewardsConfig(ctx) returns an error, overwriting defaults; change
the logic so after calling LoadLPRewardsConfig you only set ext.enabled,
ext.samplingIntervalBlocks, and ext.maxMarketsPerRun when err == nil (i.e.,
successful load), leaving the existing defaults on the extension (obtained via
GetExtension()) untouched when there is an error; use the existing logger.Warn
path for failures and perform assignments inside the success branch that
references enabled, interval, and maxMarkets (or keep the current ext.* values
otherwise).
- Around line 326-335: The config reload is guarded by the extension enabled
check in leaderEndBlock, preventing ext.reloadConfig from running when
enabled==false and making re-enabling impossible without restart; move the
reload logic that checks configReloadInterval,
atomic.LoadInt64(&ext.lastCheckedHeight) and invokes go
ext.reloadConfig(context.Background()) and
atomic.StoreInt64(&ext.lastCheckedHeight, blockHeight) to run before the enabled
guard so config can be reloaded even when the extension is currently disabled.

Comment thread extensions/tn_lp_rewards/tn_lp_rewards.go Outdated
Comment thread extensions/tn_lp_rewards/tn_lp_rewards.go Outdated
@MicBun MicBun merged commit 7e99df3 into main Feb 2, 2026
7 checks passed
@MicBun MicBun deleted the distributeLpRewards branch February 2, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant