fix(pm): round-robin batch-epoch settle scan + idle fast-path (follow-up to #139) - #140
Merged
Conversation
…cessing cap process_pm_markets() section 6 (batch epoch settle) walked every active allow_batch market in by_status (status, id) order and consumed the per-block pm_processing_cap_per_block budget (done++ and current_epoch++) for EVERY market, even ones with zero queued bets. With more active allow_batch markets than the cap (e.g. 50k+ polymarket mirror markets on testnet vs cap=200), the loop stopped after the ~cap oldest markets and never reached newly-created markets: their revealed commit-reveal bets stayed queued (status=5) forever and current_epoch never advanced. Fix: only advance current_epoch and consume the cap for markets that actually had queued (status=5) bets this epoch. Idle markets are skipped without touching done, so the scan can reach newly-created markets and their bets settle on the next epoch boundary. Existing behavior for markets with queued bets is unchanged (slippage refunds still count as work, so had_queued covers both settled and refunded paths).
…#139) Section 6 of process_pm_markets() after #139 no longer let idle markets eat the processing cap, but two gaps remained: - The scan still paid the LMSR q-vector snapshot per idle market before detecting idleness. Probe the bet index first: an idle market now costs one lower_bound (bets at a market's current epoch are queued-only, so an empty probe means idle). - The scan always restarted at the lowest id, so >= cap always-busy low-id markets could permanently starve newer ones (cheap griefing: dust batch bets on the oldest cap markets every epoch). Persist a round-robin cursor in dynamic_global_property_object (pm_batch_settle_cursor) and resume the scan there, wrapping once per boundary. The market by_status index becomes ordered_unique (status, id) so the cursor can seek mid-range and in-status iteration order is deterministic by id; all existing users do prefix lookups (scalar lower_bound / equal_range) and are unaffected. Index + dgpo field changes require a chain reset — the pm branch testnet is being reset for #139 anyway. Regression tests (consensus_sim, BUILD_CONSENSUS_TESTS): - batch_settle_idle_markets_do_not_starve: cap=1, three allow_batch markets, bet only on the newest — guards the #139 fix (fails on pre-#139 code). - batch_settle_round_robin_prevents_busy_starvation: cap=1, market 0 fed a fresh queued bet every epoch — guards the cursor (fails without it).
On1x
added a commit
that referenced
this pull request
Aug 8, 2026
…m loop (steemit#349) Adversarial follow-ups to the F1/steemit#300 early-exit review (owner-approved bundle), riding the same pm chain-reset as F1/#139/#140/#141. steemit#348 (defense-in-depth, closes the remainder of goal steemit#290): F1 guarantees `uncovered == 0` by construction, but #141 showed a regression can break it and it was charged to LP principal SILENTLY (the only diagnostic was in the no-LP subcase). Add an always-on elog in settle_liquidity whenever uncovered > 0, regardless of LP presence, so any future regression is loud in node logs / acceptance instead of eroding LP principal unseen. Log-only: deterministic, no consensus effect, no halt (a hard assert would take the chain down on an unforeseen edge — worse than charging LP and logging). steemit#349 (DoS bound): settle_market's early-exit claim-distribution loop iterated every deferred claim on a market with no cap — a griefer could spam cancels/leverage-closes to force unbounded per-block work when the market settles (same class as #139/#140). Add MAX_PM_DEFERRED_CLAIMS_PER_MARKET (10000) and a monotonic `deferred_claim_count` on pm_market_object; past the cap an early-exit skips recording its contingent claim. Conservation-safe: the skipped tail simply stays in the curve and pays 0 at settlement, exactly like bucket-exhaustion — NOT routed to forfeit_pool (that would double-count the tail, which is still in the curve → a mint). The exit itself always succeeds; only the contingent upside is forgone past the cap. Field: FC_REFLECT appended (order stable), snapshot export via reflection, import contains-guarded so it survives reimport. All touched TUs pass single-TU -fsyntax-only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the two gaps left open in the #139 review (stacked on #139 — its commit is included; merge #139 first or merge this and it lands too):
Idle fast-path. After fix(pm): stop batch-epoch settle from starving markets behind the processing cap #139 the boundary scan no longer let idle markets eat the cap, but each idle LMSR market still paid the q-vector snapshot (
outcome_countindex finds) before its idleness was detected. The bet-index probe now runs first: an idle market costs exactly onelower_bound. This is safe because bets at a market's current epoch are queued-only — settled/refunded bets always live at past epochs — so an empty probe reliably means idle.Round-robin cursor. The scan always restarted at the lowest market id, so ≥ cap always-busy low-id markets would still deterministically starve newer ones — and that state is cheap to manufacture (dust batch bets on the oldest cap markets every epoch). The scan now resumes from
pm_batch_settle_cursor(new consensus field ondynamic_global_property_object, same pattern as the pm seed flags), wrapping at most once per boundary. Cursor = next unvisited active market when the cap cuts the scan short, 0 after a full circle.The remaining ceiling is noted in-code: the walk is O(active batch markets) per boundary at one probe each; if that ever hurts, index queued bets by (status, market) and drive the scan from that.
Index change
pm_market_indexby_statusgoes fromordered_non_unique(status)toordered_unique(status, id)so the cursor can seek mid-range. All existing users do prefix lookups (scalarlower_boundin the crons,equal_range(status)inprediction_market_api) and keep working; in-status iteration order becomes deterministic-by-id instead of insertion order. Requires a chain reset — as does #139 itself (its behavior change is ungated), so the pm testnet reset covers both.Tests
Two consensus_sim regression scenarios (
BUILD_CONSENSUS_TESTS), both designed to fail on the code they guard:batch_settle_idle_markets_do_not_starve— cap=1, three allow_batch markets, a queued bet only on the newest; pre-fix(pm): stop batch-epoch settle from starving markets behind the processing cap #139 code starves it forever and bumps idle epochs (both asserted).batch_settle_round_robin_prevents_busy_starvation— cap=1, market 0 re-fed a queued bet before every epoch boundary; without the cursor market 1 never settles. Block phasing is arranged so market 0 is queued at every boundary (otherwise the idle-skip alone would mask the cursor).Verification
pm_evaluator.cpp,prediction_market_api.cpp, andtest_pm_lifecycle.cppagainst the pm-branch generated hardfork header (clang,-fsyntax-only, clean).BUILD_CONSENSUS_TESTS=ON— same CI gap already tracked from the HF14: Prediction Markets #124 review.🤖 Generated with Claude Code