A liveness bug froze the PRT dispute game; safety held, funds were Cartesi-owned, and the fix is now in.
Environment: Ethereum Mainnet — immutable, no admin/control keys
Rollup stack: Cartesi Rollup SDK · Cartesi Machine (rv64gc Linux) · App in C++ → RISC-V
App/VM initial hash: 0x615acc9fb8ae058d0e45c0d12fa10e1a6c9e645222c6fd94dfeda194ee427c14
Repos/Builds/Deployments (check wiki): https://github.com/cartesi/honeypot/
Core contracts:
- DaveConsensus (L2 Consensus):
0x6CE590b9F0697327f18c601DF6f0baE4a0801B68 - Faulty PRT Tournament:
0x83a126e4dde48ea211aa4b698ea5045ccec0cbb0
TL;DR
This honeypot fulfilled its purpose: it revealed a liveness bug in the implementation of our PRT dispute game under real mainnet conditions while preserving safety and limiting impact to Cartesi-owned funds (≈ $1,000 CTSI).
- We hit a liveness bug in the PRT dispute game during internal testing on mainnet.
- Safety was preserved: no unauthorized withdrawals, no incorrect settlement.
- Because this deployment is immutable and DaveConsensus allows only one active epoch, the stuck tournament cannot terminate, so the app is frozen and funds are locked forever (≈ $1,000 in CTSI, Cartesi Foundation funds only).
- Root cause: a contract check for “does a match exist?” reused a content field (
otherParent) as a sentinel. The dishonest claim used a Merkle subtree equal to0x000…000, so the check incorrectly concluded the match didn’t exist and timing-out the dishonest side reverted. - Fix: decouple existence from content with a storage-backed phase flag, add property-based tests for zero-heavy trees, and add monitoring for “timeout revert” and “no actionable matches.”
This was an implementation bug in the PRT smart contracts; the algorithm as specified in the papers [1, 2] remains intact.
What happened
Honeypot runs the PRT (Permissionless Refereed Tournaments) fraud-proof system. PRT uses a chess-clock: if a claimant makes no move for 1 week, the other side can win by calling winMatchByTimeout.
- We (Cartesi) intentionally submitted a fraudulent claim to test the game, 6 days after the tournament started, leaving this fraudulent claim with 1 day remaining before timeout.
- The honest node responded with a bisect, which flipped the chess clock to the dishonest claim.
- After 1 day with no action from the dishonest side, our watcher should have called
winMatchByTimeout. - When simulated, the
winMatchByTimeoutcall reverted with "match doesn't exist" error, despite the match clearly existing and being eligible for timeout. This prevented any valid move from progressing the tournament.
Because DaveConsensus manages exactly one disputed epoch at a time and opens the next epoch only after settlement, the dispute’s inability to terminate caused a permanent fail-stop for this deployment.
Impact
- Funds affected: ≈ $1,000 worth of CTSI; Cartesi Foundation funds only.
- Users: No third-party users were affected.
- Safety: Preserved — no unauthorized withdrawal, no incorrect state settlement.
- Liveness: Permanently lost for this deployment (freeze).
- Blast radius: Confined to this honeypot instance.
Timeline (UTC + chain anchors)
- 2025-09-10 01:08:11 — Tournament instantiated by DaveConsensus
- Block 23329320, tx
0x49ba24855a350e04e71ae7caf762c6d664dcd393219d9f4a310eb1faeb98698c, creating PRT Tournament0x83a126e4dde48ea211aa4b698ea5045ccec0cbb0.
- Block 23329320, tx
- 2025-09-10 01:27:35 — Honest node proposes correct result
- Block 23329416, tx
0xa21f3462cd9e04de3ec493a1da96acf3d0f79c6373a058650e08599a31f7e152.
- Block 23329416, tx
- 2025-09-12 16:28:59 — Fraudulent attempt fails (bad Merkle payload)
- Block 23348180, tx
0xa9c203183b22a89bd4a67174cbcd863789d6aad8d718ed6c5b4cc57bfa2a3e17.
- Block 23348180, tx
- 2025-09-12 16:48:35 — Second fraudulent attempt fails (bad payload)
- Block 23348277, tx
0x9fa0027dde7b6c01d8dd35316f4419b83564fc1e5c02638d0561381737737fbb.
- Block 23348277, tx
- 2025-09-16 13:20:59 — Fraudulent claim included
- Block 23375889, tx
0x0af088334c4849a807b15ba6afd493dbc92f0364b0a3b03ac2a54acee9e72c10.
- Block 23375889, tx
- 2025-09-16 13:21:23 — Honest node bisects (clock flips to dishonest side)
- Block 23375891, tx
0x6c9a222c14470ea4dcf9a11fdbf355fa47a6f77d8b8474e902e6d6058a486710.
- Block 23375891, tx
- ≈ 1 day later —
winMatchByTimeoutreverts with “match doesn’t exist” → tournament stuck; deployment frozen.
Technical root cause (in brief)
PRT stores per-match state as:
The helper:
During a double bisect, otherParent is legitimately updated to a subtree from the opponent’s commitment. In our test, the dishonest claim populated its off-path child commitment with 0x000…000, a value that is syntactically admissible, though it does not correspond to any real execution trace. Our implementation mistakenly used otherParent == 0 as a sentinel for “match not created,” so calls gated by exists(...) (including winMatchByTimeout) reverted and the tournament could not terminate.
This is an implementation error (sentinel conflation) in the PRT smart contracts, not an algorithmic failure of PRT.
Why funds are locked but not lost
- Immutable, no admin: this deployment has no multisig or escape hatches by design.
- Single active epoch: DaveConsensus opens the next epoch only after settling the current one; the dispute cannot settle; therefore the app is permanently frozen.
- Safety preserved: no function exists that could settle incorrectly or move funds without winning the dispute; the bug prevents termination, it does not grant a fraudulent win.
Fix & verification
Code change (principle): decouple existence from content.
- Introduce a storage-backed phase flag (e.g., Uninitialized → Running → Resolved) or a separate created mapping set at creation and cleared on resolution.
- Replace exists(State) with a storage read of phase == Running (or created[id] == true).
- Audit all gates/call sites that previously relied on content values.
Testing:
- Add a regression covering this incident sequence (honest proposal → two failed fraud attempts → valid fraud → honest bisect → timeout →
winMatchByTimeoutsucceeds). - Property-based tests over Merkle trees (including zero-heavy trees); assert eventual termination and that timeout paths never revert when legal.
Monitoring:
- Add dashboards/alerts for “no actionable matches > X hours”.
Next steps
- Land the fix and tests in PRT, run a fresh audit pass on the dispute state machine and timeout logic.
- Publish these artifacts and redeploy the Honeypot vNext after review.
- Continue the bug-bounty, “financial benchmark” approach with staged increases as confidence grows.
On-chain references (full)
- App/VM initial hash
0x615acc9fb8ae058d0e45c0d12fa10e1a6c9e645222c6fd94dfeda194ee427c14 - Contracts
- DaveConsensus (L2 Consensus):
0x6CE590b9F0697327f18c601DF6f0baE4a0801B68 - PRT Tournament (stuck):
0x83a126e4dde48ea211aa4b698ea5045ccec0cbb0
- DaveConsensus (L2 Consensus):
- Transactions
- Instantiate:
0x49ba24855a350e04e71ae7caf762c6d664dcd393219d9f4a310eb1faeb98698c(block 23329320; 2025-09-10 01:08:11 UTC) - Honest correct proposal:
0xa21f3462cd9e04de3ec493a1da96acf3d0f79c6373a058650e08599a31f7e152(block 23329416; 2025-09-10 01:27:35 UTC) - Fraud attempt failed #1:
0xa9c203183b22a89bd4a67174cbcd863789d6aad8d718ed6c5b4cc57bfa2a3e17(block 23348180; 2025-09-12 16:28:59 UTC) - Fraud attempt failed #2:
0x9fa0027dde7b6c01d8dd35316f4419b83564fc1e5c02638d0561381737737fbb(block 23348277; 2025-09-12 16:48:35 UTC) - Fraudulent claim included:
0x0af088334c4849a807b15ba6afd493dbc92f0364b0a3b03ac2a54acee9e72c10(block 23375889; 2025-09-16 13:20:59 UTC) - Honest bisect:
0x6c9a222c14470ea4dcf9a11fdbf355fa47a6f77d8b8474e902e6d6058a486710(block 23375891; 2025-09-16 13:21:23 UTC)
- Instantiate:




