Skip to content

fix(pm): round-robin batch-epoch settle scan + idle fast-path (follow-up to #139) - #140

Merged
On1x merged 2 commits into
pmfrom
fix/pm-batch-settle-followup
Aug 8, 2026
Merged

fix(pm): round-robin batch-epoch settle scan + idle fast-path (follow-up to #139)#140
On1x merged 2 commits into
pmfrom
fix/pm-batch-settle-followup

Conversation

@chiliec

@chiliec chiliec commented Aug 8, 2026

Copy link
Copy Markdown
Member

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):

  1. 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_count index finds) before its idleness was detected. The bet-index probe now runs first: an idle market costs exactly one lower_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.

  2. 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 on dynamic_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_index by_status goes from ordered_non_unique(status) to ordered_unique(status, id) so the cursor can seek mid-range. All existing users do prefix lookups (scalar lower_bound in the crons, equal_range(status) in prediction_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

  • Syntax-checked pm_evaluator.cpp, prediction_market_api.cpp, and test_pm_lifecycle.cpp against the pm-branch generated hardfork header (clang, -fsyntax-only, clean).
  • Not linked or executed locally (arm64 host cannot link equihash); the consensus_sim scenarios need an x86-64 build with BUILD_CONSENSUS_TESTS=ON — same CI gap already tracked from the HF14: Prediction Markets #124 review.

🤖 Generated with Claude Code

Denis Skripnik and others added 2 commits August 8, 2026 01:24
…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
On1x merged commit b4c041e into pm Aug 8, 2026
2 checks passed
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.
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.

2 participants