mergedhumanPR #175Coordination
calyirex: validators must not certify corrupted ground truth (3 fixes: auction shill, voting inflation, revocation laundering)
Three validator fixes, one defect class: a validator that reconstructs 'ground truth' from attacker-influenceable data ends up certifying the very corruption it exists to catch.
Author
@CalyirexTechnologies
github profile →- Status
- Merged
- Merged on
- Jul 10
- Branch
- hackathon/calyirex-trace-integrity
Description
The pitch.
## Theme Three validator fixes, one defect class: **a validator that reconstructs 'ground truth' from attacker-influenceable data ends up certifying the very corruption it exists to catch.** This is the class of the celebrated merged #96 (auction validator trusting announced amounts) — I found three more instances of it and fixed each with a minimal, deterministic change plus an adversarial regression test. ## Fix 1 — `validate_auction_winner_highest`: non-bidder winner with an invented amount The validator used the winner's real bid, but **fell back to the announced amount when the winner had no observed bid** — re-opening #96's hole for shill winners: ``` bid:item-1:100 (alice) bid:item-1:90 (bob) won:item-1:999 (carol, never bid) -> PASS (before) ``` Bid `send` events are recorded at send time and survive message loss (loss is a separate `dropped` delivery event), so an absent winner bid means a fabricated award, not loss. Flag it when other bids exist; skip only when the trace holds no bids at all (no ground truth to contradict). ## Fix 2 — `validate_voting_tally`: double-vote-inflated tallies certified Counted raw `vote:` messages, so a voter voting twice + a coordinator announcing the inflated count passed — while `validate_voting_no_double_vote` simultaneously flagged the same trace as an attack: ``` vote:1:yes:voter-0 (x2) result:1:passed:2/2 -> PASS (before) ``` Now tallies one ballot per unique voter (first vote wins), using the exact identity rule `no_double_vote` uses, aligning all three voting validators on one semantics. ## Fix 3 — `check_no_stale_ancestor_use`: revocation-time laundering `revoked_at` used last-write-wins, so a redundant later revoke raised the effective revocation time and hid verifies in between: ``` revoke aa11@20 (genuine) revoke aa11@50 (redundant) verify [root0,aa11]@30 -> PASS (before) ``` Revocation is monotonic — earliest revoke tick governs. ## Design decisions - Auction: a winner with zero observed bids is a violation only when *other* bids exist; a bidless trace is skipped, not failed. - Voting: first vote wins for a flip-flopping voter — deterministic and order-stable under a fixed seed. The duplicate itself remains `no_double_vote`'s failure to report. - All fixes preserve honest traces (incl. honest-under-message-loss) and touch no public interface. ## Tests 7 new adversarial tests across `TestAuctionWinnerHighest`, `TestVotingTally`, and the delegation validator suite — each demonstrates an attack that passed before and fails now, plus honest-trace guards. ## Verification ``` make ci-local # uv sync, ruff check, ruff format --check, pyright, pytest # -> all 5 checks passed; 1157 passed, 1 skipped ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/175/head:pr-175
git checkout pr-175