Conversation
…ront-running protection - Introduce two optional execution modes per bet: batch (mode 1) and commit-reveal (mode 2) - Preserve instant per-bet execution (mode 0) as default for best UX - Define global and per-market parameters for configuration and governance control - Implement batch epochs with unified uniform-price settlement to prevent intra-batch front-running - Add commit-reveal operations: bet commitment, reveal, penalty for no-reveal with transfer into winners' pool - Design batch settlement math preserving liquidity provider invariant and fairness via canonical CPMM aggregation - Add database schema changes for bets, commitments, and market flags supporting new modes and states - Extend market logs to track batch-related actions: commit, reveal, batch settlement, and forfeits - Specify layered defenses against front-running including epoch snapshot pricing and min_tokens enforcement - Define phased rollout plan: batch mode first, then commit-reveal, followed by tiering and client policies - Document open questions and future improvements like encrypted sealed bids (mode 3) and cancellation policy
…off strategy - Add new chain_properties versions 4 (hf13) and 5 (hf14 PM) for governance params - Introduce 40+ Prediction Market consensus parameters including fees, disputes, batch settings, cron budget, lazy pool, and leverage - Define market fee structure and governance cap rules focusing on oracle fee cap - Document versioning, median calculation, and validator publishing rules for new params - Implement leverage fund as sub-allocation of lazy pool free balance with detailed accounting and protocol state transitions - Present full mathematical leverage model: CPMM/LMSR calculations, cancel values, liquidation thresholds, safety margin, and leverage constraints - Describe atomic liquidation mechanism ensuring pool protection before opposing bets execute, including cascade logic and cascade loop handling - Analyze risk types and mitigation: price-movement risk elimination via atomic liquidation, safety margins, and dynamic caps; outcome risk remains inherent - Provide detailed architecture overview, fund allocation parameters, frontend UI terminology (Boost vs Leverage), and risk analysis - Outline protocol operations, API endpoints, database schema, pre-calculation and slider logic for leverage positions - Address MEV considerations for liquidation rebalancing and sandwich attacks with mitigation plans for VIZ DLT implementation
…ion by role - Add comprehensive README outlining canonical scenarios for normal and disputed resolves - Document master ledger calculations for payout distributions and zero-sum properties - Include role-specific subfolders with interaction diagrams and signed/virtual operations - Provide detailed token flow tables for normal and disputed market outcomes - Outline leverage mechanics including open, close, liquidation, and settlement steps - Describe dispute scenarios: disputer wins, loses, and forced auto-close with penalties - Verify all operations and virtual operations present in code with references - Add instructions on how to observe states and events via API plugin methods - Cover edge cases like time penalties, liquidations, and refund mechanics - Provide an index of workflow document folders by participant role for easy navigation
- Updated fc submodule pointer from 99b5d133 to 5a9d84a1 - Ensured thirdparty dependencies are current and consistent
- Added mermaid package version 11.4.1 - Added vitepress-plugin-mermaid version 2.0.17 for Mermaid integration - Added multiple new dependencies related to mermaid and diagram rendering - Included types packages for d3 and related libraries for better type support - Updated package-lock.json to reflect new dependencies and their versions - Removed several optional and deprecated dependencies to clean up lock file
- Increase total number of hardforks from 13 to 14 - Introduce hardfork 14 with features for prediction markets (binary CPMM, multi LMSR) - Add oracles, dispute mechanisms, batch/commit-reveal, and lazy liquidity pools - Define placeholder activation times for mainnet and testnet - Use version 4.0.0 for hardfork 14 release candidate
- Add chain_properties_pm median evaluator and integrate into median calculation - Implement lazy-pool logic for DAO-committee voting weight in committee_processing - Register PM evaluators for all PM operations in database initialization - Add core indexes for PM-related objects for consensus and chain state tracking - Implement PM liquidity settlement, market resolution, leverage liquidation, and recall mechanics - Add HF14 hardfork initialization including lazy-liquidity pool singleton creation - Update CMakeLists.txt to include PM source and header files with proper compiler flags - Enhance database.cpp with PM processing hooks and vote weight adjustments for lazy pool stake - Provide detailed internal helpers for PM operation including settle market, refund bets, and liquidity allocation
…ive APIs - Implement prediction_market_api plugin with full lifecycle management - Provide APIs to query markets, outcomes, bets, positions, and liquidity - Support account leverage positions and creator ban status retrieval - Offer oracle info and list oracles with reliability scoring - Include dispute info and vote tallying with projected verdict calculation - Record and prune metadata and kline time-series data on block application - Add support for lazy pool and deposit queries along with chain properties - Implement metadata parsing and filtered market listing by category and jurisdiction - Integrate with chain plugin database and handle post-operation kline recording - Configure plugin with pmm-ttl-days option for metadata retention days - Setup CMake build configuration for prediction_market_api plugin library
- Introduce functions to import pm_oracle, pm_market, pm_outcome, and pm_dispute objects with shared_string member handling - Add export and import logic for all HF14 PM related indices in snapshot processing - Clear existing PM objects before importing new ones during database initialization - Enhance snapshot deserialization to handle absent PM objects in pre-HF14 snapshots - Log import counts for each PM object type during snapshot loading to aid diagnostics
- Include prediction_market_api plugin in wallet build dependencies - Add remote_prediction_market_api binding with optional connection handling - Implement pm_api() accessor for prediction_market_api proxy with assertion - Introduce prediction market helper methods for oracle registration, update, market creation, bet placement, commitment hashing, bet commit/reveal/cancel, liquidity management, market resolution, dispute creation/voting/resolution, position transfer, lazy deposit/withdraw, and leverage operations - Add read API passthrough methods for markets, oracles, bets, positions, liquidity, disputes, lazy pool, chain properties, market metadata, and market kline data - Extend fc::api remote_node_api.hpp with prediction market API message signature class and FC_API definition - Update wallet.hpp and wallet.cpp with full prediction market API support and method declarations - Ensure wallet starts normally even if prediction_market_api plugin is unavailable on connected node
- Include prediction_market_api in CMakeLists.txt for vizd program - Add prediction_market_api header inclusion in main.cpp - Register prediction_market_api plugin in appbase application initialization code
…ests - Add tests covering oracle registration after HF14 activation - Implement full binary market lifecycle: creation, betting, resolution, payout - Test committee dispute scenario with outcome overturning via voting - Verify lazy-pool stake contributes to dispute voting weight and quorum - Ensure external oracle rejection refunds seed liquidity exactly once - Add bet cancellation scenario reversing CPMM reserves and bets sum - Adjust consensus_sim harness to support new pm tests and sanitizer flags conditionally - Expose direct database access in simulated_node for test assertions - Fix simulated_node block witness field to validator for accuracy in tests
…operties support - Introduce HF13 distribution epoch length and HF14 prediction markets features in governance docs - Add detailed median-voted HF14 prediction-market parameters and kill-switch flags explanation - Add new `prediction_market_api` plugin with extensive JSON-RPC read-only methods for markets, bets, oracles, disputes, lazy pool, and governance data access - Include computed DTOs and charting support for prediction markets with offset-from-newest pagination - Document prediction market concepts analysis comparing Onix protocol to theoretical models - Update advanced hardfork and chain properties docs to cover new prediction market functionality
- Introduce readonly JSON-RPC plugin for HF14 prediction markets state access - Document market-related API methods including markets, outcomes, bets, liquidity, and metadata - Describe position, leverage, oracle, dispute, lazy pool, and governance methods - Provide details on kline/time series for market weight history and pagination approach - Explain computed DTOs representing bets, oracles, votes, and payout structures - Include example usage and code snippets for API calls and data processing - Link to relevant protocol operations and chain property documentation docs(governance): update chain properties with HF13 and PM parameters - Add chain_properties_hf13 with distribution_epoch_length parameter - Introduce chain_properties_pm (v5) for ~30 prediction market parameters and kill-switch flags - Detail all median-voted parameters for oracle, market, batch, dispute, time penalty, lazy pool, leverage, and fairness - Clarify live kill-switch flags to disable commit-reveal, lazy pool, or leverage without hardfork docs(advanced): extend hardfork management with HF13 and prediction markets - Add entries for HF13 epoch length and HF14 prediction markets including CPMM/LMSR, oracles, disputes, commit-reveal, lazy-pool, and chain properties v5 docs(prediction-markets): add comprehensive analysis of conceptual mapping of Onix PM protocol - Provide detailed table comparing 90 theoretical prediction market concepts against VIZ Onix on-chain implementation - Categorize concepts as solved, inherent, not needed, partial/roadmap, client layer, or open risks - Discuss information theory, mechanism design, liquidity and trading aspects in depth - Highlight Onix innovations: risk-free LP, CPMM binary, LMSR multi, commit-reveal batch bets, optional leverage subsystem, lazy pool governance voting weight - Explain architectural decisions omitting orderbooks, combinatorial markets, and peer prediction
- Updated chainbase submodule commit from 39ab2c2 to d429230 - Ensures third-party library is aligned with latest upstream changes
…cycle, coverage floors and thin-client APIs Consensus (HF14 follow-up ops, appended so operation indices stay stable): - pm_dispute_oracle_respond (op 22): the market oracle posts a public rebuttal onto an open dispute; stored on the dispute object (public-hearing model), allowed only while open and within oracle_response_deadline, re-post overwrites. - pm_unban (op 23): the resolver that imposed an account-mode ban (banned_by) may lift it early; sets banned_until to epoch and clears banned_by. - pm_ban_expired (virtual): the per-block cron sweeps temporary oracle/creator bans at banned_until, clears them and emits the lift for history/indexers. On-chain state: - pm_market gains decision_url/decision_reason — the oracle's resolution statement stored on-chain (set by pm_resolve_market / pm_no_contest reason), readable via get_market with no history scan. - pm_resolve_market_operation gains decision_reason (reflected on the wire). - pm_dispute gains oracle_response/oracle_response_time. - pm_oracle and pm_creator_ban gain banned_by; pm_creator_ban gains a by_ban_expiry index so the cron sweeps expired bans oldest-first (cleared bans sort into the 0-bucket, permanent bans past now, both skipped). Chain properties (witness-median tunables): - pm_listing_min_coverage_percent (2.5x): hide under-insured markets from the default catalog (enforced by the API plugin, revealed via show_risky). - pm_betting_min_coverage_percent (1.5x, advisory): client risk-confirm threshold; validated betting <= listing. Thin-client read APIs (non-consensus, for the viz-js client): - get_leverage_quote / get_leverage_close_preview / get_leverage_convert_preview reuse the frozen pm::leverage math to mirror the open/close/convert evaluators. - get_market_categories (taxonomy + live counts), get_market_full (one-call enriched, account-scoped), get_lazy_allocations / get_market_lazy_allocation. - Wallet remote_node_api bindings for all of the above. Docs & tests: - EN + ru + zh-CN docs updated (chain-properties, prediction-market-api, specification, operations overview/prediction-markets/validators, virtual-operations); library-integration spec + thin-client plan added. - test_pm_lifecycle: cases #58-#63 cover oracle rebuttal + decision_reason, no-contest rationale, manual unban and its guards, and ban auto-expiry vop.
…throughs The cli_wallet build failed because remote_prediction_market_api and the wallet_api pm_get_*/pm_list_* methods returned the node's typed objects. Those chainbase state objects (pm_market_object, pm_bet_object, ...) and the API DTOs embedding them are not default-constructible (deleted default ctor / shared_string members require a segment manager), so fc::api's client deserializer (T tmp; var.as<T>()) could not instantiate them. Return fc::variant instead: the node already emits fully-formed JSON and cli_wallet prints the variant unchanged, so the read surface is identical.
…te_node_api cli_wallet failed to compile because remote_node_api.hpp pulled in <graphene/plugins/prediction_market_api/prediction_market_api.hpp> transitively, but programs/cli_wallet has no include path to that plugin. After the read pass-throughs switched to fc::variant, the header (and the pmapi alias) are no longer referenced anywhere in the wallet, so remove them. graphene_wallet still builds; the public wallet header no longer leaks a plugin-only dependency.
…LP fee Add two HF14 median-voted consensus parameters and their enforcement: - pm_oracle_accept_window_sec (default 1h): a pending market the named oracle never accepts nor rejects is voided by the per-block cron once now >= created_time + window. The creators seed liquidity is refunded (return_liquidity); the non-refundable creation fee stays with the DAO fund. Tracked via a new pm_market_object.accept_deadline field and a by_accept_deadline index; emits the new pm_market_expired virtual op (op-id 101, appended last in the operation variant to keep tags stable). - pm_lazy_min_liquidity_fee_percent (default 2%): the lazy pool skips markets whose liquidity_fee_percent is below this reward floor, so it never subsidizes depth it is not paid enough to provide. Wired into calc_median and chain_properties_pm::validate().
… fee Cover the new pm_oracle_accept_window_sec / pm_market_expired lifecycle and the pm_lazy_min_liquidity_fee_percent reward-floor gate across: - EN docs (chain-properties, specification, operations, virtual-operations) - RU and zh-CN localizations (@l10n) at full parity with the EN source - library integration spec (delta section + property/vop tables, op-id 101) and thin-client plan - Onix paper EN + RU (state machine, acceptance flow, lazy-pool gate); PDFs rebuilt via pandoc + xelatex (EN 30pp, RU 32pp, 0 missing glyphs).
- Added warning that the live protocol uses basis points (bp), not permille (‰) - Explained the conversion from original PHP prototype’s permille to bp in on-chain code - Specified that all fee fields (oracle_fee_percent, creator_fee_percent, liquidity_fee_percent, etc.) use bp (10000 = 100%) - Highlighted the use of `fromBP` parser for fee fields and rejection of markets exceeding fee sum 10000 - Warned that using deprecated `fromPermille` leads to incorrect fee values, off by a factor of 10
…line The ?: between time_point_sec() and (now + fc::seconds(...)) has no common type — the latter yields fc::time_point, and each type converts to the other, which GCC rejects as ambiguous. Wrap the second branch in an explicit time_point_sec(), matching the copy-init conversion already used for the reveal/dispute deadlines in this file.
…erations The generic impacted-account visitor only collected signing authorities, so prediction-market events were missing from the histories of accounts that did not sign them: - signed ops lost their counterparties (pm_create_market -> oracle, pm_transfer_position -> recipient, pm_unban -> target, oracle auto-accept whitelist); - virtual ops carry no authority at all, so payouts, forfeits, liquidations, oracle penalties, market accept/expire and ban expiry were invisible to the affected users. Add explicit get_impacted_account_visitor overloads for the PM user and virtual operations, inserting every account field they carry. Market-only virtual ops that reference a market by id but carry no account name (pm_batch_settle / pm_dispute_finalize / pm_dispute_auto_close / pm_lazy_recall) are intentionally left to the generic handler.
|
Addressed the account-history review blocker in 3eebfd6: added explicit |
…, per-node) The free-form `metadata` JSON was stored in the consensus `pm_market_object` (shared_string) permanently — never pruned — even though consensus never reads it (it is written once and only parsed off-chain by the prediction_market_api plugin). That let a market permanently bloat every node's chainbase/shared memory with unbounded, unvalidated data. Move it out of consensus entirely: - pm_market_object: drop the `metadata` field (member, ctor, FC_REFLECT). The operation `pm_create_market_operation.metadata` is unchanged — clients still send it and it lives in the block log, exactly like custom_operation.json. - pm_create_market_evaluator: stop persisting metadata into state. - prediction_market_api: ingest metadata off-chain from the create operation (post_apply_operation) into the existing prunable pm_market_meta_object, instead of reading it back from the consensus object in on_block. - snapshot: drop the metadata import/export for pm_market (auto-excluded from the reflected dump; import of legacy snapshots ignores the field). Because it is now non-consensus, each node prunes it on its own schedule via --pmm-ttl-days (default lowered 7 -> 5; 0 keeps it forever for archival nodes). No consensus length/UTF-8 cap is needed — the blob no longer touches state.
|
Addressed the unbounded on-chain |
…xed retention A resolved+settled market (status 3, payout_status 3) is immutable — no betting, dispute, resolve or payout can touch it again; it only lingered in chainbase "for history", growing shared-memory state without bound. process_pm_markets() now GCs such markets and their whole object cluster (outcomes, bets, liquidity, commits, dispute votes, leverage positions, the dispute and lazy-allocation rows) once they have been closed for a FIXED protocol constant PM_CLOSED_MARKET_RETENTION_SEC = 5 days (measured from result_expiration + dispute grace). The retention is hardcoded and identical on every node, so pruning is fully deterministic: every node deletes exactly the same markets at the same block, keeping shared-memory state and snapshots in lock-step network-wide (a node syncing from a snapshot ends up with the same market set as everyone else). Work is bounded by the existing per-block cap. Only status-3/payout-3 markets are collected; disputed (payout_status 2) and never-settled markets are left untouched. Nothing holds an id-reference to a settled market, so there are no dangling references after removal.
|
Follow-up (31e8aab): resolved markets are now garbage-collected from consensus state. A settled market (status 3 / payout_status 3) is immutable, so |
…ones Extend the market garbage collector to reclaim ANY dead market a fixed 5 days after it becomes terminal — not only resolved+paid ones. A market is dead once nothing can act on it: resolved and paid out, void/no-contest, oracle-rejected, or the oracle never accepted and the accept window expired. To anchor the retention on the actual moment of death (rather than the declared result_expiration), add a `finalized_time` field to pm_market_object, set to the head-block time at every terminal transition: - oracle rejects the market (status -1) - accept window expires, market voided (pm_market_expired) - oracle misses resolution, refund (pm_oracle_missed_penalty) - dispute auto-close refund - settlement / auto-payout (covers resolved, no-contest, post-dispute) A new by_finalized index (finalized_time, id) lets process_pm_markets() sweep terminal markets in time order, skipping the finalized_time==0 live bucket, and delete each cluster PM_CLOSED_MARKET_RETENTION_SEC (5 days) later. Retention is a fixed protocol constant identical on every node, so pruning stays deterministic and snapshots identical network-wide. Snapshot import reads finalized_time when present. Work stays bounded by the per-block cap.
…D, part 1) pm_processing_cap_per_block counts markets, not work: gc_market() deleted a market's entire object cluster inside the single block where retention expired, so one unit of cap could mean a million row deletions. Turn it into gc_market_step(db, mkt, budget): it drops at most `budget` objects out of the block's global pm_settle_rows_per_block allowance and returns true only once the whole cluster, market object included, is gone. A market that runs out keeps its place at the head of the sweep (finalized_time never changes) and resumes next block — no cursor is needed because every range is re-entered at its lower_bound and removed rows are gone. The forfeit_pool burn is now zeroed in the same step. Collection can span blocks and a burn that left the field standing would fire again on every re-entry, burning the same tokens repeatedly and pushing current_supply below the accounted sum. A half-collected market is inert (terminal status, no operation can reach it) and the rows dropped hold no money, so the supply invariant is flat across the pause. Test gc_row_budget_spans_blocks: a 143-row cluster with the budget at its floor of 100 must take more than one block and never lose more than 100 rows in one. Checked against an unbounded control, where it reports the pre-fix behaviour. Also documents that pm_add_liquidity mints an unfloored row per call — the fourth row source, to be budgeted with the settlement phase machine.
…pool hazard (steemit#432 D) Every operation that creates, splits or removes a bet row asserts mkt.status == 1, so a settling market (status 3) has a frozen row set — phase 2 walks exactly what phase 4 pays, no extra gate needed. The one interaction that is NOT automatic: cron §1 forfeits unrevealed commitments into forfeit_pool of any market regardless of status. Harmless today (§1 runs before §5 in the same block) but a multi-block settlement can see forfeit_pool grow after phase 3 folded it into winners_pool, orphaning those tokens until the GC burn. Phase 5 has to route a mid-flight forfeit_pool rather than assume zero.
D) Reading settle_market() end to end fixes the exact state a paused settlement must carry: the split is two-pass, so both the phase-2 aggregates and the phase-4 running totals have to survive a block boundary, not just a cursor. Those twelve fields do not belong on pm_market_object — every market that ever existed would carry them so that the handful currently settling can use them. They go on a pm_settlement_object created on entry and removed at finalization, so the cost tracks settlements in flight. Per-winner payout depends only on winners_pool, weight_total and the row itself, which is what makes the cut clean; the void path reuses the same accumulators and keeps today's last-participant-absorbs-the-remainder rounding. Also records why the escrow accumulator is signed: phase 3 pays early-exit claims out of a pot whose rows are still standing, so it legitimately goes negative before phase 4 releases them.
…ate (steemit#432 D) Scaffolding only — nothing creates one yet. Settling a market is a two-pass job (aggregate the losing stake and the winners' curve weight, then turn those into per-row payouts), so once the per-block row budget can cut it at any point, both passes' partial results have to survive the pause: cursor, phase, aggregates, running totals and the conservation accumulator. That state lives in its own object rather than a dozen fields on pm_market_object, which every market that ever existed would carry so the handful currently settling can use them. It is created when the cron picks a market up and removed at finalization, so the cost tracks settlements in flight. The object type is appended at the END of the enum — inserting it anywhere else would shift every later object-type id and invalidate existing snapshots. Snapshot plumbing lands with it, because a snapshot can now catch a settlement mid-flight: exported, imported under a contains() guard, cleared before a hot reload (M1 lesson — leftover rows collide on by_id), and dropped by the GC sweep with the rest of the market cluster. The supply invariant counts `escrow` as PM-held and prints it in the breakdown, so a mid-settlement snapshot balances; the sum is zero today since no settlement object exists. graphene_chain + graphene_snapshot build clean, pm_lifecycle_suite green (63).
…h row source) Owner decision q#661=A. pm_add_liquidity mints a NEW pm_liquidity_object on every call — contributions are not merged per provider — and settle_liquidity walks all of them, twice. The evaluator asserted only "amount > 0", so liquidity was a way to mint settlement rows at 1 raw apiece while all three bet paths were already floored. Use pm_min_liquidity, the floor that already gates creating a market, so the minimum ticket for putting up liquidity does not depend on whether you open the market or top it up later. No new parameter. Test min_liquidity_floor_applies_to_topups: a top-up one raw below the floor and a 1-raw top-up are both rejected with no row minted, and — the control that makes those meaningful — a top-up exactly at the floor is accepted. Existing suite liquidity is added at 2x and 4x the floor, so nothing else moves. Suite green (64). Documented in the LP guide (en + ru) and in settlement-work-bounds.md, which no longer lists liquidity as a known gap.
…fix D part 2) settle_market() becomes settle_market_step(db, mkt, budget), resumed block after block from the pm_settlement_object cursor and spending the same global row budget (pm_settle_rows_per_block) as collection. Phases: 1 force-close, 2 aggregate (refund queued rows, losers_sum, winner weight, rows_total), 3 deferred claims and split constants, 4 payouts, 5 fees, liquidity, dust, finalize. Only phases 2-4 are metered: a bet row costs pm_min_bet (1 VIZ) to create, while the liquidity and leverage rows walked in phases 1 and 5 cost pm_min_liquidity (100 VIZ), so those are bounded economically. Three resume-specific hazards closed: - "last participant absorbs the remainder" no longer peeks at the rest of the market (wrong under resume, and quadratic); rows_total/rows_done from phase 2. - the void path burns leftover pots only after subtracting them from escrow, otherwise the finalize assert trips on its own bookkeeping. - the markets_in_dispute_window gauge is decremented on first entry rather than on every block of the flight. The signed escrow accumulator holds money released from a row but not yet paid; the PM supply invariant counts it as held and finalize asserts it reaches zero. Test: settle_row_budget_spans_blocks (140 rows, budget at its floor of 100) checks the settlement spans blocks, terminates at most budget rows per block, shows payout_status 4 in flight, pays every row, moves bettor balances by exactly the sum the rows recorded and removes the settlement object. Verified against an unbounded control, which reports the pre-fix "140 rows in a single block, settled in 1 block".
refund_all_bets() walked every bet row of a market in the block where the deadline lapsed and built an in-memory vector of every participant on top of it, in both void paths: cron section 2 (missed resolution) and section 3 (dispute auto-close). It is replaced by refund_market_step(db, mkt, budget), running on the same pm_settlement_object phase machine and the same global row budget as settlement and collection. Two metered passes over one predicate (bet status 0/5/6): pass one only measures (stake total, row count), pass two refunds the stake and pays each row its pro-rata slice of the forfeit pool. Pass one changes nothing, so pass two re-walks exactly the set pass one counted - that is what keeps "who is being refunded by this void" answerable across a pause without tagging rows or holding a vector. The pool moves into the settlement escrow on entry, so the supply invariant keeps counting it while it is paid out over several blocks. payout_status 4 becomes a gate rather than a display value: pm_resolve_market, pm_no_contest and pm_transfer_position refuse a market being refunded, cron section 4 steps over disputes whose market section 3 already claimed, and the batch executor skips it so queued rows cannot move between the two passes. Fixes a pre-existing ordering bug found while reviewing this diff: the old path drained forfeit_pool before return_liquidity(), which force-closes leveraged positions and routes their curve residual back INTO forfeit_pool. Those tokens then rode on the market row until GC dropped it, stranding them in current_supply with no owner - the exact leak the void routing exists to prevent. Liquidity is now returned first and the leftover accounted for after. Test: void_refund_row_budget_spans_blocks (140 rows, budget at its floor of 100, no oracle resolution) checks the void spans blocks, refunds at most the budget per block, shows payout_status 4 in flight and ends with status 3, resolved_outcome -1, every row refunded and the bettors up by exactly the stake. Verified against an unbounded control reproducing the pre-fix "140 rows in a single block". Full suite 66 cases green.
…led markets (steemit#432) Cron §6 filled every row queued into a market's current epoch within one block, paying a single unit of the market-counting cap. After fix A a queued row costs pm_min_bet (1 VIZ) — the same price as the bet rows settlement was bounded for — and on LMSR markets each row additionally pays for a curve quote, so it is the dearer of the two. §6 now draws on the shared pm_settle_rows_per_block budget, charged per row *visited*: a visit is the work the block actually does. Because leftover rows are matched by epoch, two things follow. The epoch counter may only advance once the queue is drained — bumping it mid-drain would strand the remaining rows with their stake already debited. And the executor must run off the epoch boundary while a pass is in flight, or already-revealed stakes would wait a whole epoch window for the next boundary. The resume point is a second dgp cursor, pm_batch_settle_bet_cursor, consumed by the first market the round-robin scan visits (which is pm_batch_settle_cursor by construction) and imported from snapshots under a contains guard; losing it is safe, the pass restarts at the head of the epoch and skips executed rows by status. Entry is gated on row_budget > 0: §6 runs last, so a settle-heavy block can reach it with nothing left, and running zero iterations would fall through to the persist step and write a zero cursor over the parked one. Separately, by_result_expiration was keyed (status, result_expiration, id). A settled market keeps status 3 with result_expiration in the past, so it sat at the head of the range the §5 settle sweep walks — and skipping a row costs no cap, so the loop never stopped early on it. Measured on the testnet snapshot of block 82641602: 48971 iterations, 48942 of them pure continue, with only 29 markets actually owing a settlement. The key becomes (status, finalized_time, result_expiration, id); finalized_time is stamped exactly once, so finalized_time == 0 means "still owes work" and a market leaves the group the moment it settles. payout_status is deliberately left out — settlement flips it 1 -> 4 mid-flight and must not move the row it is resuming. Index keys are not serialized, so no snapshot migration is needed. Covered by batch_queue_row_budget_spans_blocks (260 rows, budget at the consensus floor of 100): the drain spans more than one block, no block executes more than the budget, the epoch stands still until the last row is gone, and the cursor is released afterwards. With the budget bypassed the test reproduces the pre-fix behaviour — "260 rows drained in 1 block(s)". Full pm_lifecycle suite: 67 cases, green.
Cron §4 finalizes disputes whose voting window closed. For each one it walks every ballot of the disputed market to build the stake-weighted tally, while charging a single unit of the market-counting cap. M3 caps ballots per market at MAX_PM_DISPUTE_VOTES_PER_MARKET, but that bounds one market, not a block: with cap markets finalized per block the ceiling was cap x 10k rows, three orders of magnitude past the budget every other sweep respects after steemit#432. A tally cannot be resumed the way settlement is - the verdict needs every ballot at once, and parking the partial per-outcome sums would mean carrying a vector on the dispute row. So the budget is enforced between disputes: one starts only while budget is left, and is then charged for the ballots it walked. Worst case per block becomes row_budget + one market's ballot cap. Deferring a finalize is economically inert, since pm_dispute_vote refuses ballots past voting_end_time. Covered by dispute_tally_row_budget_defers_next: two disputes carrying 120 ballots each against a budget of 100 finalize in different blocks, and both markets still reach settlement. With the guard removed the same test reports both tallies running in one block. Full suite: 68 pm_lifecycle cases green.
… vote pm_dispute_vote enforced MAX_PM_DISPUTE_VOTES_PER_MARKET by walking every ballot of the market on each incoming vote: O(n) per vote, O(n^2) per market, and that work is per-transaction, outside the per-block sweep budget added for cron section 4. Keep the count on pm_dispute_object.ballots (appended to the struct and to FC_REFLECT) and increment it only when a ballot row is created; a revision overwrites the row and must not move the counter, so the field counts rows, not votes. There is no individual removal - GC drops the whole dispute cluster - so no decrement path exists. Snapshots need one step that open_commits did not: disputes are imported before their ballots, so a contains-guard alone cannot seed the field. reconcile_pm_dispute_ballots() runs after the pm_dispute_vote import and walks the vote index once, which both seeds pre-field snapshots and catches drift in new ones. A snapshot taken mid-GC can legally hold fewer ballot rows than the counter; reconciling downwards is harmless there because the market can no longer be voted on. Test: dispute_ballot_counter_matches_rows compares the counter against a live row count after every ballot and pins the revision path. Control run (incrementing on revision) fails it as expected. Full consensus_sim module green: 101 cases, pm_lifecycle 69.
Every section of process_pm_markets() charges one shared counter against pm_processing_cap_per_block, so section order is priority order. Section 7, the lazy-pool recall step, charges that counter for every allocation row it visits - including rows it only inspects and leaves alone, which is what keeps the scan bounded. Its working set is large and long-lived: 34548 status-0 allocations on the testnet at block 82646702 against a cap of 200. The loop therefore exhausts the budget every block, and section 8 behind it never ran at all: expired oracle and creator bans kept a stale banned_until and pm_ban_expired was never emitted. Nobody stayed blocked - enforcement compares banned_until against now rather than testing the field for emptiness - so the damage was the stored state, the history event, and any client reading "banned" as "field is non-zero". Fix is a private counter rather than reordering or a bigger shared cap. The sweep is self-clearing: a visit zeroes banned_until, which drops the row out of the swept range for good, so per-block work equals the number of bans that just expired and the cap bounds even a synchronized burst. A note under section 7 warns that anything appended below it on the shared counter is dead on arrival. Test: ban_expiry_survives_saturated_cron_budget reproduces the starvation in miniature - cap 2, three live lazy allocations to saturate it, one short creator ban that must still lapse. Control run with the sweep back on the shared counter fails on exactly that assertion. Full consensus_sim module green: 102 cases.
…e rejected open-position sweep (4.10) 4.8 also folds in the lazy-pool withdraw queue candidate so the audit trail lives in one place rather than only in the goal tracker.
…e per-transaction re-walks
list_markets skipped `from` raw index positions, then filtered under-insured oracles' markets out of the page it built. The two counts disagree, so a caller paging with from += limit lands back inside a run of hidden markets, the fill loop walks out of that run to the same visible markets as before, and the feed repeats the identical page forever instead of advancing. Reproduced live on the testnet, where kalshi's 27037 active markets are hidden by policy (insurance 0): paging status=1 with limit=50 over offsets 11400..11900 returned 550 rows holding only 51 distinct markets - from offset 11550 on, every page was byte-identical to the previous one. Count `from` in rows the caller actually saw, in all three branches (oldest / newest / expiration). This is what every other listing here already does - list_markets_by_category and list_markets_by_event decrement `from` after their filters - so list_markets was the odd one out. The risk-floor verdict is memoized per (oracle, head block), so charging the skip loop for it costs one map lookup per skipped row.
…#678) service_lazy_withdraw_queue() drained the whole FIFO withdraw queue on every call — up to 150M rows when free_balance was large — and it was called from six places, four of them inside evaluators, so one capital-return transaction could consume unbounded per-block work (one row minted per partial withdrawal for as little as 1 raw, no floor). - service_lazy_withdraw_queue(db, row_limit) returns how many rows it processed and stops after row_limit. - Per-transaction call-sites pass 1 (pay the FIFO head; the bulk is drained by the cron). - New cron section 9 drains the queue up to the shared per-block pm_settle_rows_per_block row budget whenever pending_withdrawals > 0, charging row_budget honestly. This is the liveness backstop: the queue keeps progressing even when no capital returns to free_balance. Test lazy_withdraw_queue_row_budget_spans_blocks: 250 one-raw rows against a 100-row budget span several blocks at <= budget/block, FIFO preserved, free >= 0, pending -> 0. The honest control (pre-fix unbounded drain) fails the per-block bound. No layout change -> no testnet redeploy. docs: settlement-work-bounds 4.8 (queue budget) + lazy-pool guide (the queue now progresses every block via the cron, not only on capital returns).
…er + pm_lp_payout vop (steemit#681) spec said `LP fee pool = max(0, liq_fee − liquidity_fee_earned)` but the code never assigned liquidity_fee_earned; early-exit LPs earn no commission (pm_withdraw_liquidity needs status==0, earned_fee is set with status==3) and void markets earn none either (bonus==0). Fix the spec (en/ru/zh) to match the code and make the income observable: - settle_liquidity writes m.liquidity_fee_earned = bonus (total LP income: liq_fee + time-penalty pool + undistributed dust), a post-hoc counter that round-trips through the snapshot as a plain reflected field — no layout change. - emit one pm_lp_payout virtual op per DIRECT LP (account, market_id, principal returned, income, F1 uncovered charge). Lazy-pool allocations have an empty provider, return to the pool's free_balance and stay market-level only. account_history overload guarded by op.account.size(). - test lp_payout_vop_and_fee_counter: single direct LP gets the whole bonus, counter == liq_fee, principal == 2×min_liquidity, charge == 0.
…se 1/5 loud signals DoS review follow-up (owner C, 2026-08-20), three residuals: - P0: §6 walked every active market per boundary to find queued bets; idle markets paid a free probe so the scan grew linearly with the number of markets. Add by_status_market (status, market, id) on pm_bet and drive the scan from queued (status=5) rows — idle markets are never visited. Resume cursors keep their meaning (executed rows leave the index). Defensive skip over status-5 rows that leak onto a non-active market. - P1: purge_deferred_claims built an in-memory vector of every claim before deleting it (the refund_all_bets pattern). Remove in place, advancing the iterator before db.remove. - P2: settle phases 1 (force_close_positions) and 5 (settle_liquidity) walk LP/position rows unmetered, bounded only by the 100 VIZ floor. Emit a log-only signal when a walk overshoots pm_settle_rows_per_block so a future floor change makes the phase visibly expensive. Docs: settlement-work-bounds.md §4.11–4.12. consensus_sim pm_lifecycle 72/72 green (incl. batch_settle_idle_markets_do_not_starve).
…rlay-spec, settlement-work-bounds
…osed Add §7 'Attack vector closed' to settlement-work-bounds.md and §18 'Block-Work Bounds and Spam Resistance' to specification.md, in all three locales (en/ru/zh-CN).
- committee_vote: O(1) re-vote lookup via unique (voter, request_id) index - expire_award_shares: stop at first non-expired entry - account auction expiration: walk (account_on_auction, start_time) composite - account recovery: sweep master_authority_history by last_valid_time
…q#686) Raise MAX_PM_DISPUTE_VOTES_PER_MARKET 10k->100k and add a per-ballot effective-vesting floor MAX_PM_DISPUTE_VOTE_MIN_VESTING (1000.000 VIZ, == the dispute fee). The row cap bounds the tally/read work; the floor makes each row expensive so a Sybil cannot fill the cap with dust-stake accounts and lock out real holders. Checked in pm_dispute_vote (create and revise), expressed in the same unit the tally weighs (effective_vesting_shares) so the gate matches what actually counts.
…687) Mirror the PM-dispute gate on committee_request votes (owner: analogous cap + stake cap, gated on HF14): cap NEW ballots per request at MAX_COMMITTEE_VOTES_PER_REQUEST (100k) and require a per-voter vesting floor MAX_COMMITTEE_VOTE_MIN_VESTING (1000.000 VIZ == dispute fee), both gated on CHAIN_HARDFORK_14 so pre-fork sub-floor voters are unaffected. The request finalize cron and get_committee_request_votes walk every ballot, so the cap bounds that work; the floor makes each row expensive.
Update settlement-work-bounds.md (en/ru/zh-CN) to reflect MAX_PM_DISPUTE_VOTES_PER_MARKET raised 10 000→100 000 and the new MAX_PM_DISPUTE_VOTE_MIN_VESTING (1000 VIZ). Document why the floor is not delegation-bypassable (5-day revocation lockup) and why the finalize-time weight read is deliberate (prevents weight-multiplication).
Add MAX_COMMITTEE_VOTES_PER_REQUEST (100 000) and MAX_COMMITTEE_VOTE_MIN_VESTING (1000 VIZ) to the committee_vote_request_operation reference (en/ru/zh-CN).
Move four vote limits out of #define constants and into chain properties so validators can govern them via versioned_chain_properties_update: - committee_votes_per_request (100000) and committee_vote_min_vesting (1000.000 VIZ) -> chain_properties_hf9 (base, inherited by hf13/pm) - pm_dispute_votes_per_market (100000) and pm_dispute_vote_min_vesting (1000.000 VIZ) -> chain_properties_pm Defaults equal the previous constants, so on-chain behavior is unchanged; only governability changes. Fields are appended at the end of each struct's FC_REFLECT_DERIVED and wired into the median loop (HF9 block for committee, HF14 block for pm), and the committee/pm evaluators read them from median_props. Enforcement remains HF14-gated. Add boundary probes for all four in props_validate_test.cpp. Remove the now unused #defines from config.hpp / config_testnet.hpp.
The four vote limits moved from #define constants to chain properties: committee_votes_per_request + committee_vote_min_vesting (base hf9) and pm_dispute_votes_per_market + pm_dispute_vote_min_vesting (pm v5). Update governance/chain-properties (new "Committee Voting" section + hf9/pm field lists), the specification's Disputes table, protocol/operations/committee.md and settlement-work-bounds.md to reference the new median-voted field names.
…ntact) committee_votes_per_request / committee_vote_min_vesting were added to the base chain_properties_hf9 struct, but HF9 is already live on mainnet — appending fields there re-serializes existing validator versioned_chain_properties votes with a new positional layout and breaks pre-HF14 committee voters. Move both caps into chain_properties_pm (HF14-only) next to the dispute caps, and median-vote them in the HF14 block (not HF9). Enforcement stays HF14-gated in committee_evaluator. hf9 struct + FC_REFLECT_DERIVED reverted to original.
Summary
Introduces Hardfork 14 (HF14): Prediction Markets — a full on-chain
prediction-market subsystem for the VIZ blockchain, plus the read-only APIs,
wallet bindings, snapshot support, tests and documentation around it.
The feature is hardfork-gated (
CHAIN_HARDFORK_14) and preserves the zero-sumtoken invariant: prediction markets never mint or burn tokens,
current_supplyis untouched, and settlement conserves value across bets, LP and forfeit pools.
What's included
Consensus / chain
bets, liquidity, resolution, disputes, leverage) gated behind HF14.
pm_dispute_oracle_respond), early ban lift(
pm_unban) and automatic ban expiry (pm_ban_expiredvirtual op via theper-block cron).
decision_url/decision_reason)and dispute oracle responses, readable without history scans.
coverage floors for listing (2.5x) and betting (1.5x, advisory).
Plugin / wallet / snapshot
prediction_market_apiplugin with extensive read APIs: market/oracle/dispute queries, category taxonomy + live counts, klines, one-call enriched
market view, and non-consensus leverage previews (quote/close/convert) that
reuse the in-node margin math.
remote_node_apibindings for the PM read APIs.Docs & tests
operations, virtual operations) and workflow/integration design docs.
test_pm_lifecycleconsensus-sim suite covering the fullmarket lifecycle, disputes, leverage, bans and settlement invariants.
Notes
master. Feature branch:pm(16 commits).