feat(soroban): decentralized dispute resolution with staked jury voting & slashing - #28
Conversation
Please resolve conflicts in the following files: soroban-contracts/Cargo.toml |
…voting & slashing Model dispute resolution as an on-chain state machine resolved by a staked jury using commit-reveal voting, with reward distribution to the majority and slashing of the minority and no-shows — a trust-minimized counterpart to escrow's single-admin resolve_dispute. - open_dispute (admin) starts time-boxed commit/reveal phases derived from the ledger clock so phases can't drift out of sync. - commit_vote stakes collateral and stores sha256(salt || vote || juror_xdr), binding the commitment to the juror's address so a copycat can't replay someone else's commitment. - reveal_vote checks the hash and tallies the vote; resolve tallies the majority and fixes the per-winner slashed-pot share. - withdraw uses a pull pattern (no unbounded loops) with checks-effects-interactions; ties and below-quorum turnouts refund everyone with no slashing. Includes 27 unit tests covering the full lifecycle, both verdicts, no-show slashing, tie/quorum refunds, out-of-phase rejection, and adversarial paths (copycat commitments, wrong reveals, repeated-withdraw draining). Documents the storage layout, errors, model, and security limits in the workspace README and registers the crate in the workspace.
b7257ec to
2ff11d5
Compare
|
@meshackyaro thanks for the review — conflicts are now resolved. ✅ I rebased
The PR now shows as mergeable. Re-verified locally after the rebase: 27/27 tests pass, |
This is a substantial addition — a full commit-reveal jury contract — and the design writeup makes it easy to follow the trust model. Nice work anticipating the obvious attacks (copycat commitment replay, unbounded-loop withdrawal, repeated-withdrawal draining) and writing tests specifically for them rather than just the happy path. On the state machine Deriving phase from now against the two deadlines instead of storing an enum is the right call — agreed that a stored phase field is just another thing that can desync from the clock it's supposed to track. Good judgment call to note it explicitly rather than let it look like an oversight. On resolve/withdraw Permissionless resolve plus pull-pattern withdraw with checks-effects-interactions is the right pattern for a jury of unbounded size — glad this wasn't done with a loop over all jurors sending payouts directly. On the stated limitations Appreciate that the README is upfront about what this doesn't defend against — a well-capitalized actor funding many jurors, and honest-minority-gets-slashed-too. These are real, non-cosmetic limitations for a dispute system handling real stakes, not nitpicks: On tests and CI 27 tests covering both verdicts, no-show slashing, tie/quorum refunds (including non-revealers, which is easy to miss), out-of-phase rejection for every transition, and the adversarial paths (double-everything, wrong vote/salt, copycat replay, repeated-withdrawal draining) is a strong set — this reads like someone thought about how they'd attack their own contract, which is what I want to see on something holding stake. Nothing here blocks approval in principle — the core mechanism looks sound and well-tested. I'd like answers on the quorum/tie mechanics and a decision (fix now vs. tracked issue) on the sybil-resistance and honest-minority-slashing limitations before merging, given this is handling real staked funds once deployed. |
|
Thanks for the thorough review — the questions land exactly where the security-relevant decisions are. Answers below with line references, and a fix-now-vs-tracked call on the two limitations. Address binding (your double-check)Confirmed: every path uses the caller's own authenticated address, never a separately-spoofable parameter.
So to reveal record Quorum thresholdIt's Your instinct is right that one fixed Tie conditionExact lines are } else if dispute.yes_count > dispute.no_count {
(Outcome::Plaintiff, dispute.yes_count)
} else if dispute.no_count > dispute.yes_count {
(Outcome::Defendant, dispute.no_count)
} else {
(Outcome::Tie, 0u32)
};Because there are only two sides, "more revealed votes than the other side" is strictly The two limitations — my recommendation: track for v2, don't fix hereBoth are already documented loudly in the README's Security considerations / known limitations section (the "Jury sybil / stake-weighting" and "No appeals and majority-takes-all slashing" bullets), so they're integrator-visible where the contract is documented, not just buried in this PR description. Sybil-resistance / weighted or random jury selection — recommending tracked v2, explicitly out of scope for v1, mirroring how signer rotation was deferred in the governance-guard PR. v1 is deliberately one-stake-one-vote with an open permissionless jury; it resists free sybils (real collateral per identity) but not a well-capitalized actor. Worth noting there's already a concrete building block for the weighting version: the sybil-resistant weighted reputation scoring from signed attestations added in #20 is the natural score/selection input for a reputation-weighted jury. If you agree, I'll open a tracking issue framing v2 as "reputation-weighted / randomized jury selection, built on #20" and link it from the README bullet so it doesn't read as abandoned. Honest-minority-slashing — this is an intentional design choice, and I'd like the reasoning on record rather than treating it as a bug: it's a Schelling-point coordination game (Kleros-style). Commit-reveal is precisely what makes slashing the minority defensible — jurors can't observe and copy the leader, so the only way to reliably land in the majority is to independently predict the honest/focal outcome. Remove minority-slashing and you remove the incentive to vote carefully at all. The accepted cost is that in a genuinely close or ambiguous call, an honest juror on the losing side is slashed alongside malicious ones. For v1 that's a known rough edge, not a correctness bug; the softening levers (margin-based partial refunds inside a small spread, or an appeal round) are exactly the kind of thing an appeals/v2 design should own. I'll fold this into the same v2 tracking issue if you'd like. Single-round / no-appeals / unaudited are all v1-visible in the same README section. If you want them louder still (e.g., a one-line " Net: nothing here changes the core mechanism — I'd like to (1) open one v2 tracking issue covering sybil-resistant/weighted selection (on #20) and the honest-minority softening, link it from the README, and (2) optionally add a per-dispute-config note. Let me know if you'd rather I pull per-dispute quorum into this PR instead of deferring it. |
Address binding, quorum mechanics, and the tie condition are all resolved for me — thanks for the line-level walkthrough, that's exactly what I needed to sign off on those without re-deriving it myself. On the two open items: Warning banner — yes, please add it in this PR. One-line, top of the dispute-resolution interface section, something like " V2 tracking issue — please open it now and link it from both README limitation bullets (sybil/weighted-selection and honest-minority-slashing) as part of this PR's diff, not as a follow-up promise. Same bar we held the governance-guard PR to with #27. Go ahead and frame it the way you described — reputation-weighted/randomized selection built on #20, plus the minority-slashing softening levers (margin-based partial refunds, appeal round) as candidate v2 directions, not commitments. Per-dispute config — this is the one I want to slow down on. You've correctly identified that juror_stake and min_jurors being contract-level and immutable means the real deployment model is one instance per stake tier. Before I file that under "clean v2 change, defer it," I want to know: what's the actual launch plan? If we're expecting disputes across a wide range of stake sizes from day one, standing up and maintaining N contract instances is real operational overhead, and I'd rather we decide that's acceptable on purpose than back into it because deferring config felt like the natural default. If early usage is going to be narrow (one or two stake bands), current scope is fine and I'm happy to defer. Can you give me a sense of what the initial deployment target looks like? That answer decides whether per-dispute config comes into this PR or stays in the v2 issue. |
… issue Add an integrator-facing v1/unaudited/no-appeals banner at the top of the dispute-resolution interface section, and link the v2 tracking issue (workman-labs#29) from both the jury-sybil and majority-takes-all-slashing limitation bullets so the deferred sybil-resistant/weighted selection and honest-minority softening read as scoped follow-ups rather than gaps.
|
Both README items are done and in this PR's diff now (commit 45e7f70), not as follow-up promises:
Per-dispute config — straight answer: the launch target isn't pinned yetI don't want to file this under "narrow, so defer" and have you sign off on a scope decision that's really just me guessing. The honest state is the initial stake-band breadth isn't decided, so I can't tell you today whether day-one usage is one or two bands or a wide range — which is exactly the input your question turns on. Given that, I'd rather not back into either outcome. My recommendation:
So the decision I'm actually asking you to make: is it acceptable to merge v1 on the current per-instance-per-stake-tier model with the config question tracked-and-open, on the understanding that a wide-stake-range launch requires making this call (per-dispute config vs. deliberately running N instances) before that launch, not before this merge? If yes, we're unblocked. If you'd rather not merge until the launch scope is pinned, I can pull that decision forward — but that's a product/launch-planning call on our side, not a contract-correctness one, so I didn't want to hold the mechanism hostage to it without checking. If you'd prefer it tracked as its own issue rather than a sub-note on #29, say so and I'll split it out. |
Both README items look good — the banner is direct and the tracking issue is well-scoped and properly cross-linked. Appreciate you not letting either wait on the config question. On per-dispute config: you're right to push back rather than let me sign off on a scope decision neither of us can actually make yet. Your reasoning holds — the mechanism itself is correct and complete regardless of the config question, the change stays cheap whenever we do land it, and tying the merge of a working, well-tested contract to an unrelated launch-planning decision would be the wrong kind of caution. So: yes, merging v1 on the current per-instance-per-stake-tier model is acceptable, with the explicit understanding that before any wide-stake-range launch, someone has to make the per-dispute-config-vs-N-instances call — and that gate lives at the launch-planning stage, not here. I'll take that as an action item on our side; it shouldn't block you. On tracking: keep it as the sub-note on #29 rather than splitting it out. It's genuinely related context for whoever picks up the v2 work, and splitting it into its own issue right now would just create a second thing to keep in sync with a decision that isn't ready to be made yet. If it turns out to need its own timeline once the launch target is pinned, we can split it then. This is good to merge from my side. Thanks for the thorough back-and-forth on this one — the README and issue trail you've left make this easy for the next person to pick up context on, which matters as much as the contract code for something handling real stakes. GREAT JOB AND WELL DONE! |
There was a problem hiding this comment.
One more thing before I merge.
This whole thread is a good example of how to work through review on security-sensitive code: precise answers, honest "I don't know yet" instead of guessing and documentation left in a state the next person can actually use.
Appreciated the rigor across all of this. Merging now, and thanks for your honest contributions
Closes #18
What
Adds a new
dispute-resolutionSoroban contract that models dispute resolution as an on-chain state machine resolved by a staked jury using commit-reveal voting, with reward distribution to the majority and slashing of the minority and no-shows. It is the trust-minimized counterpart toescrow's single-adminresolve_dispute.Lifecycle
Two time-boxed phases are derived from the ledger clock against the dispute's two deadlines (not stored as an enum, so they can't drift out of sync):
now <= commit_deadline): a juror stakesjuror_stakeand submitscommitment = sha256(salt || vote_byte || juror_xdr). Binding the juror's own address into the preimage stops a copycat from replaying someone else's commitment — the copied hash can never be revealed from a different address.commit_deadline < now <= reveal_deadline): the juror discloses(vote, salt); the contract recomputes the hash and, on a match, records the vote and bumps the running tally.now > reveal_deadline, permissionless): tallies the majority and fixes the per-winner slashed-pot share.juror_stake + reward_per_winner; losers/no-shows are slashed; ties and below-quorum turnouts refund every staker with no slashing.Tests (27, all passing)
Full commit→reveal→resolve→withdraw lifecycle for both verdicts; no-show slashing feeding the winners' pot; tie and quorum-failure refunds (including non-revealers); out-of-phase rejection for every transition; and adversarial paths — double init/commit/reveal/resolve/withdraw, wrong-vote/wrong-salt reveals, a copycat replaying another juror's commitment being unable to reveal it, and a slashed loser never draining the pot via repeated withdrawals.
Checklist
cargo test— 27/27 passcargo clippy -- -D warnings— cleancargo fmt --check— clean (new crate)wasm32v1-nonerelease build succeeds--workspaceCI (Soroban CI already caches cargo deps viaSwatinem/rust-cache)Notes / limitations (see README)
Resists free sybils (real collateral per identity) and hidden-vote manipulation (commit-reveal), but does not defend against a well-capitalized actor funding many jurors — there's no random jury selection or reputation weighting. Single-round with no appeals; honest minority jurors are slashed alongside malicious ones. Unaudited.