feat(events): emit per-loser outcome loss events on settlement (Closes #168)#1
Open
CodingAngel1 wants to merge 1 commit into
Open
feat(events): emit per-loser outcome loss events on settlement (Closes #168)#1CodingAngel1 wants to merge 1 commit into
CodingAngel1 wants to merge 1 commit into
Conversation
…TevaLabs#168) Adds a per-loser ("outcome","loss") event during competitive settlement so analytics, user-notification services, and indexers no longer need to infer losses from the absence of payout events. Emitted in 4 settlement paths (UpDown indexed + legacy, Precision indexed + legacy). Suppressed on refund paths (price-unchanged, one-sided pool, min participants fallback, admin cancellation) -- those emit their own refund events instead. Payload: (user, round_id, mode=0|1, amount, side|0, predicted_price|0) - UpDown: side carries losing direction (0=Up,1=Down); predicted_price=0 - Precision: predicted_price carries revealed guess; side=0; for unrevealed commitments predicted_price is published as 0 (guess unknowable on-chain). In _resolve_precision_mode, the winner/loser-mark loop now caches a per-participant Vec<bool> is_winner_mask alongside the existing amount/price snapshots, so the loser branch reads O(N) winner state instead of an O(N^2) winners.iter().any() lookup. Drift between the four parallel caches is impossible by construction (each iteration pushes exactly once), so snapshot lookups use unwrap() rather than unwrap_or(0) -- a 0-stake loss event from drift would silently corrupt downstream accounting. Tests: 7 new tests in tests::resolution covering all 4 settlement paths, 3 negative paths, and a cross-mode count-invariant test. Docs: EVENT_SCHEMA.md gains the canonical ("outcome","loss") entry; event_schema_guide.md is updated to summarise the addition. Refs TevaLabs#168
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 TevaLabs#168 — emits an explicit
("outcome", "loss")event per losing participant during competitive settlement, complementing the implicit winner signal frompending-winningsaccumulation and the explicit("round", "fallback")/("round", "cancelled")refund events. Analytics, user-notification services, and indexers no longer need to infer losses from the absence of payout events.Why this matters
Before this PR there was no on-chain log of losses at all. Observers had to maintain a derived view: "user did not appear in any
("claim","winnings")AND had a position in this round → loss." This breaks on batches, partial claims, lost keys, and any team that is reading events fresh. The new event makes loss a first-class observable.What changed
New event
("outcome", "loss")—(user, round_id, mode, amount, side, predicted_price)sidecarries the user's failing direction (0=Up,1=Down).predicted_priceis fixed at0(the field is only meaningful in Precision mode).predicted_pricecarries the user's revealed guess (4-decimal scale).sideis fixed at0. For participants who only committed and did not reveal,predicted_priceis published as0— the guess is unknowable on-chain until reveal — documented in the schema as a uniform-payload convention.Emitted on every competitive settlement path
_record_winnings_indexed— primary UpDown after-migration path_record_winnings_legacy— UpDown legacy bulk-map fallback (migration data)_resolve_precision_mode— primary Precision after-migration path_resolve_precision_legacy— Precision legacy bulk-map fallback (migration data)Suppressed on every refund/cancel path — no loss event is emitted when stakes are returned whole. Those cases use their own events instead:
final_price == start_price→ emits nothing extra (refund to pending winnings, may be claimed via("claim","winnings"))("pool","onesided")("round","fallback")("round","cancelled")_resolve_precision_modeprecision optimisation — the loser branch now caches a per-participantVec<bool> is_winner_maskin the same winner-detection pass that already cached(amount, predicted_price). The loser branch now reads O(N) winner state instead of the previous O(N²)winners.iter().any(|w| w.user == user)lookup. Snapshot accesses on the cache use.unwrap(); the four parallel caches (participants,participant_amounts,participant_prices,is_winner_mask) are pushed exactly once per first-pass iteration so drift between the two passes is impossible — were it to ever occur,.unwrap()would fail loudly rather than silently publish a 0-stake loss event that would corrupt downstream accounting.Schema & docs
docs/EVENT_SCHEMA.md— new("outcome","loss")entry; explicit note that this is an additive change at Issue Observability: emit explicit participant loss outcome events during settlement TevaLabs/Xelma-Blockchain#168 and the schema version stays at v1 (per the additive-events-don't-bump-version policy at the top of the file).docs/event_schema_guide.md— new section 8 with the same payload shape, mode-specific semantics, and the explicit non-emission rules for refund paths.Tests — 7 new tests under
tests::resolution::LOSS OUTCOME EVENT TESTS (Issue #168):test_outcome_loss_event_updown_indexed_path— 4 participants, 2 winners, 2 losers → exactly 2 loss events; per-loser assertions on(mode, round_id, amount, side).test_outcome_loss_event_updown_legacy_path— drives the legacyUpDownPositionsbulk map to take the legacy winnings path; asserts 1 loss event withside=1(Down loser).test_outcome_loss_event_precision_indexed_path— 3 participants (1 winner + 1 revealed loser + 1 unrevealed loser) → asserts that the unrevealed commit loser is still emitted withpredicted_price=0.test_outcome_loss_event_precision_legacy_path— drives the legacyPrecisionPositionsbulk map; asserts per-loser(amount, predicted_price)and explicitly verifies that winners never appear in loss events.test_outcome_loss_event_not_emitted_on_refund—final_price == start_price→ 0 loss events.test_outcome_loss_event_not_emitted_on_min_participants_fallback—min_participantsnot met → 0 loss events.test_outcome_loss_event_not_emitted_on_cancel— admincancel_round→ 0 loss events.test_outcome_loss_event_count_matches_outcomes_across_modes— runs an UpDown round then a Precision round in the same fixture and asserts (a) the total loss-event count matches the per-round loser count, (b) the winner never appears in loss events, (c) UpDown loss events are ordered before Precision loss events within a single test, locking the inter-round ordering invariant.Two helper functions live alongside the tests:
count_outcome_loss_events(env)filtersenv.events()by topic, andcollect_outcome_loss_events(env)decodes the(Address, u64, u32, I128, u32, u128)payload for indexed assertions.Linked issues
Validation
Note: the local environment used to author this PR did not have a Cargo toolchain on PATH, so
cargo check / clippy / fmtwere not run locally. CI will run the standard validation matrix.cargo test --workspacecargo clippy --workspace --all-targets -- -D warningscargo fmt --all -- --checkcd bindings && npm ci && npm run buildStatus fields left unchecked because CI must run the suite. Please run the matrix before merge.
Schema impact
The schema change is additive under the protocol's "new optional events do not bump the version" rule (see top of
docs/EVENT_SCHEMA.md). Existing indexers keep working unchanged — they will simply ignore("outcome","loss")until they opt in. New indexers / dApps gain a single, deterministic place to learn about each losing participant.Edge cases covered
mode=0/mode=1) per round, including the unrevealed-commitment loser in Precision.env.as_contractto seed the oldUpDownPositions/PrecisionPositionsbulk maps and verify the loss event still fires on those paths._update_stats_loss: Documented invariant in the contract comments; ordering-within-round is observable in the test ledger.Governance checklist
CONTRIBUTING.mdfor workflow expectationsCODEOWNERSimpact for touched paths (contracts/src/contract.rs,contracts/src/tests/resolution.rs,docs/EVENT_SCHEMA.md,docs/event_schema_guide.md)SUPPORT.mddisclosure guidance — no security-sensitive surface is changed (additive event only; existing refund/cancel/claim flows are unchanged)Reviewer notes
_resolve_precision_modeis the most invasive change; the additional state (is_winner_mask) is purely local to that function and has no contract ABI impact.cargo test -p virtual-token-contract tests::resolution::outcome(targets bothtest_outcome_loss_event_*and the adjacent resolution tests).