Skip to content

Title: feat(dispute): commit-reveal juror voting to prevent vote copying & bribery (#52) - #60

Merged
meshackyaro merged 2 commits into
trustflow-protocol:mainfrom
rhemy-arc:rhemy
Aug 20, 2026
Merged

Title: feat(dispute): commit-reveal juror voting to prevent vote copying & bribery (#52)#60
meshackyaro merged 2 commits into
trustflow-protocol:mainfrom
rhemy-arc:rhemy

Conversation

@rhemy-arc

Copy link
Copy Markdown
Contributor

Body:
Closes #59

Summary

Replaces direct vote casting (cast_vote) with a two-phase commit-reveal
scheme so that no juror can see how anyone else voted before their own
vote is permanently locked on-chain. This eliminates the vote-copying and
bribery vectors that previously made the slashing mechanism ineffective
against rational jurors.

Changes

  • Commit phase (commit_vote): Jurors submit sha256(vote_byte ++ salt)
    during a ~1-day commit window. The commitment reveals nothing about the
    vote direction.
  • Reveal phase (reveal_vote): After the commit window closes, jurors
    disclose (vote, salt) during a separate ~1-day reveal window. The
    contract recomputes the hash and rejects mismatches.
  • Resolution (resolve_dispute): Only revealed votes are tallied. A
    juror who commits but never reveals simply forfeits their vote — they
    are neither counted nor slashed.
  • Deadline enforcement: Each DisputeRecord now carries its own
    commit_deadline and reveal_deadline (ledger sequences computed at
    raise_dispute time), gating all three voting entrypoints.
  • New error variants: CommitPhaseEnded, RevealPhaseNotOpen,
    AlreadyCommitted, NoCommitFound, InvalidReveal, RevealPhaseNotEnded.
  • Events: VoteCommitted (no vote info, just that a commitment exists)
    and VoteRevealed (includes vote direction) emitted at each phase.
  • Storage: New DataKey::JurorCommit(VoteKey) for commitment hashes.
    DisputeVoters is now populated only by reveal_vote, not commit_vote.

Testing

62 tests passing (cargo test -p trustflow), including 9 new tests:

Test What it verifies
test_commit_vote_requires_stake Juror with 0 stake gets InsufficientStake
test_commit_vote_duplicate_rejected Double-commit returns AlreadyCommitted
test_commit_vote_after_commit_deadline_rejected Late commit returns CommitPhaseEnded
test_reveal_vote_before_commit_deadline_rejected Early reveal returns RevealPhaseNotOpen
test_reveal_vote_wrong_preimage_rejected Hash mismatch returns InvalidReveal
test_reveal_vote_duplicate_rejected Double reveal returns AlreadyVoted
test_reveal_vote_without_commit_rejected No commitment returns NoCommitFound
test_resolve_dispute_before_reveal_deadline_rejected Early resolve returns RevealPhaseNotEnded
test_committed_but_unrevealed_vote_not_counted Silent committer: no vote counted, no slash applied
All existing tests (staking, slashing, tie-breaking, TTL bumps, milestone
releases, partial-release settlement, fee snapshots) updated to use the
commit-reveal flow and continue to pass.

Tradeoffs

  • Commit-without-reveal is not penalised. A juror who commits but
    never reveals simply loses their vote. An alternative design would slash
    non-revealers to incentivise follow-through, but that adds complexity
    (jurors might lose their key or go offline) and the current design is
    sufficient to prevent the vote-copying attack — which is the actual goal.
  • Fixed 1-day windows. Both commit and reveal windows are hardcoded at
    17,280 ledgers (~1 day). These could be made admin-configurable per
    dispute in a follow-up, but a fixed window is simpler and matches the
    expected juror response cadence.
  • No juror selection. Any address with a positive stake can vote on
    any dispute. This is a pre-existing design choice unrelated to this PR.

Architecture

The voting flow is:
raise_dispute → commit window: ~1 day → reveal window: ~1 day → resolve_dispute
commit_vote() reveal_vote()
Hash format: sha256(vote_byte ++ salt) where vote_byte is 1u8 for
depositor, 0u8 for beneficiary. The 32-byte salt is chosen by the juror
off-chain and kept secret until reveal.

Out of scope

  • Configurable window durations
  • Penalty for non-revealers
  • Juror selection / randomisation
  • Frontend / SDK changes

@meshackyaro

Copy link
Copy Markdown
Contributor

Body: Closes #59

Summary

Replaces direct vote casting (cast_vote) with a two-phase commit-reveal scheme so that no juror can see how anyone else voted before their own vote is permanently locked on-chain. This eliminates the vote-copying and bribery vectors that previously made the slashing mechanism ineffective against rational jurors.

Changes

  • Commit phase (commit_vote): Jurors submit sha256(vote_byte ++ salt)
    during a ~1-day commit window. The commitment reveals nothing about the
    vote direction.
  • Reveal phase (reveal_vote): After the commit window closes, jurors
    disclose (vote, salt) during a separate ~1-day reveal window. The
    contract recomputes the hash and rejects mismatches.
  • Resolution (resolve_dispute): Only revealed votes are tallied. A
    juror who commits but never reveals simply forfeits their vote — they
    are neither counted nor slashed.
  • Deadline enforcement: Each DisputeRecord now carries its own
    commit_deadline and reveal_deadline (ledger sequences computed at
    raise_dispute time), gating all three voting entrypoints.
  • New error variants: CommitPhaseEnded, RevealPhaseNotOpen,
    AlreadyCommitted, NoCommitFound, InvalidReveal, RevealPhaseNotEnded.
  • Events: VoteCommitted (no vote info, just that a commitment exists)
    and VoteRevealed (includes vote direction) emitted at each phase.
  • Storage: New DataKey::JurorCommit(VoteKey) for commitment hashes.
    DisputeVoters is now populated only by reveal_vote, not commit_vote.

Testing

62 tests passing (cargo test -p trustflow), including 9 new tests:

Test What it verifies
test_commit_vote_requires_stake Juror with 0 stake gets InsufficientStake
test_commit_vote_duplicate_rejected Double-commit returns AlreadyCommitted
test_commit_vote_after_commit_deadline_rejected Late commit returns CommitPhaseEnded
test_reveal_vote_before_commit_deadline_rejected Early reveal returns RevealPhaseNotOpen
test_reveal_vote_wrong_preimage_rejected Hash mismatch returns InvalidReveal
test_reveal_vote_duplicate_rejected Double reveal returns AlreadyVoted
test_reveal_vote_without_commit_rejected No commitment returns NoCommitFound
test_resolve_dispute_before_reveal_deadline_rejected Early resolve returns RevealPhaseNotEnded
test_committed_but_unrevealed_vote_not_counted Silent committer: no vote counted, no slash applied
All existing tests (staking, slashing, tie-breaking, TTL bumps, milestone
releases, partial-release settlement, fee snapshots) updated to use the
commit-reveal flow and continue to pass.

Tradeoffs

  • Commit-without-reveal is not penalised. A juror who commits but
    never reveals simply loses their vote. An alternative design would slash
    non-revealers to incentivise follow-through, but that adds complexity
    (jurors might lose their key or go offline) and the current design is
    sufficient to prevent the vote-copying attack — which is the actual goal.
  • Fixed 1-day windows. Both commit and reveal windows are hardcoded at
    17,280 ledgers (~1 day). These could be made admin-configurable per
    dispute in a follow-up, but a fixed window is simpler and matches the
    expected juror response cadence.
  • No juror selection. Any address with a positive stake can vote on
    any dispute. This is a pre-existing design choice unrelated to this PR.

Architecture

The voting flow is: raise_dispute → commit window: ~1 day → reveal window: ~1 day → resolve_dispute commit_vote() reveal_vote() Hash format: sha256(vote_byte ++ salt) where vote_byte is 1u8 for depositor, 0u8 for beneficiary. The 32-byte salt is chosen by the juror off-chain and kept secret until reveal.

Out of scope

  • Configurable window durations
  • Penalty for non-revealers
  • Juror selection / randomisation
  • Frontend / SDK changes

I'll approve after addressing two small, testable issues and adding a short doc/changelog entry. Overall this is a clear, well-tested implementation of commit-reveal voting that removes the vote-copying/bribery vector — great work.

Merge readiness & risk assessment

Positive:
Tests cover the new flow including edge cases (duplicate commits, early reveals, wrong preimage, silent committers).
Implementation follows a simple, auditable commit-reveal pattern and documents tradeoffs in the PR description.
Risks / blockers:
Small validation/storage items below that I consider important to fix before merging (they are concrete code changes and map to the voting/commit storage logic).
In contracts/trustflow/src/lib.rs, ensure the commitment/reveal paths validate salt length and format (reject non-32-byte salts) in both commit_vote and reveal_vote to avoid ambiguous hashes or accidental mismatch vectors.
In contracts/trustflow/src/lib.rs, after a successful reveal, clear the juror's commitment entry (DataKey::JurorCommit) to avoid wasted storage and prevent reuse/replay across later calls.
In contracts/trustflow/src/lib.rs, add an explicit check that a reveal cannot be replayed or duplicated (defensive check around AlreadyVoted/NoCommitFound) and ensure the storage update order prevents a window where a second reveal could succeed.

Possible improvements (non-blocking, recommended)

Add inline code comments (or doc-comments above the relevant functions) documenting the exact hash preimage format: sha256(vote_byte ++ salt), including the required salt length and endianness/concatenation semantics. This makes it explicit for integrators who will prepare salts/off-chain tooling.
Expose the commit/reveal window durations as named constants (if not already) and add a short comment explaining why 17,280 ledgers was chosen; consider making them admin-configurable in a follow-up PR and add TODO markers in the code.
Add a short changelog / migration note in the repo (e.g., CHANGELOG.md or RELEASE_NOTES) describing the new voting flow, the fact that commit-without-reveal is not penalised, and any implications for wallets/keepers (so integrators know they must implement a two-step UX).
Add a test that attempts to reuse the same (vote, salt) preimage across two different disputes (if not already covered) to verify that commitments are keyed by dispute ID + juror and do not collide across disputes.
Consider adding a gas/storage cost note in the code comments or PR description (commit storage + reveal and clearing) so callers/keepers can reason about costs.

Notes and reasoning for the above items

Salt validation is important: a wrong-length/format salt could lead to surprising behavior (mismatched hashes) or inconsistent client implementations. Rejecting incorrect salts early gives clearer error semantics to off-chain tooling.
Clearing the commitment after a reveal is both a gas/storage optimization and reduces risk of accidental reuse or confusion. It also makes auditing on-chain state easier.
Documentation and a changelog entry are small but important because this changes the on-chain dispute UX; integrators/wallets should be able to follow the new flow and tests easily.

@rhemy-arc

Copy link
Copy Markdown
Contributor Author

Kindly review @meshackyaro

Replace direct cast_vote with two-phase commit-reveal voting to prevent
vote-copying and bribery. Addresses all maintainer review feedback:
- Clear JurorCommit after reveal to free storage and prevent reuse
- Reorder AlreadyVoted check before NoCommitFound for correct errors
- Add detailed hash preimage format documentation (sha256(vote_byte ++ salt))
- Add TODO marker for admin-configurable commit/reveal windows
- Add test: same salt across disputes does not collide
- Add test: commitment cleared after reveal
- Add CHANGELOG.md
@meshackyaro

Copy link
Copy Markdown
Contributor

Kindly review @meshackyaro

Thank you — this is an excellent, well-scoped improvement.

What I like

  • The commit-reveal design cleanly and directly addresses the vote-copying / bribery vector that previously undermined slashing incentives. The approach is simple, well-reasoned, and minimal in surface area.
  • Implementation looks straightforward and consistent: new entrypoints (commit_vote, reveal_vote), per-dispute commit/reveal deadlines, and the separate resolution step make the flow easy to understand and audit.
  • I appreciate the added events (VoteCommitted, VoteRevealed) and the focused storage layout (JurorCommit + reveal-only population of DisputeVoters). These choices preserve on-chain privacy during commit while keeping final tallies auditable.
  • Tests: great job updating the test-suite and adding targeted tests that cover the new failure modes and happy paths — the fact that all tests pass and you added explicit tests for duplicate commits, timing windows, invalid reveals, and unrevealed committers gives me high confidence.
  • The PR description and tradeoffs section are thorough and transparent; you’ve documented reasoning and limitations clearly.

Minor suggestions / next steps (non-blocking)

  • Consider adding a short docs snippet or README section showing the exact commit-hash computation and a recommended salt-generation approach (e.g., example command or small helper snippet). That will help integrators and jurors avoid mistakes.
  • It would be useful to record gas-cost/benchmark numbers for the new operations so we can confirm the cost profile (especially for bulk dispute workloads).
  • The fixed 1-day windows are fine as a first step — creating a follow-up issue to make them configurable (and to discuss whether to slash non-revealers) would be a good idea so this remains extensible.
  • Add a brief changelog entry / release note so downstream users know to update their off-chain tooling (commit generation / reveal procedures).

Final verdict

  • This is a solid improvement and I’m happy to approve it. Please consider the small docs/bench follow-ups, but nothing here blocks merging from my perspective — great work!

@meshackyaro meshackyaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this is an excellent, well-scoped change.

  • The commit-reveal design is a clear, well-justified fix for the vote-copying / bribery vector; replacing direct voting with sha256(vote || salt) + reveal is the right approach and is implemented cleanly.
  • I appreciate the per-dispute commit/reveal deadlines, the new, focused error variants, and the minimal-but-sufficient event surface (VoteCommitted / VoteRevealed). These make the flow auditable and developer-friendly.
  • Tests look thorough: all existing tests adapted and 9 new tests added (62 passing) — that gives me strong confidence in correctness and backwards compatibility of unaffected functionality (staking, slashing, settlement, etc.).
  • Storage and API changes are clear (DataKey::JurorCommit, DisputeVoters populated on reveal). Naming and comments are readable and consistent with project style.
  • Tradeoffs are well-explained in the PR description — keeping non-reveals unpunished (they simply forfeit their vote) is reasonable for now and keeps complexity down.

Overall: this is a security-forward, well-tested improvement that I'm happy to approve. This is now ready to merge.

@meshackyaro
meshackyaro merged commit ca046cb into trustflow-protocol:main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement commit-reveal voting for cast_vote to prevent juror vote-copying

2 participants