Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions contracts/dice-duel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
mock-game-hub = { path = "../mock-game-hub", features = ["testutils"] }
sha2 = "0.10.9"
stellar-xdr = "27.0.0"
194 changes: 125 additions & 69 deletions contracts/dice-duel/README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,154 @@
# Dice Duel Game

A two-player dice game smart contract built on Stellar's Soroban platform.

## Overview

Each player commits to a roll. After both players have rolled, the contract
reveals two dice for each player and the highest total wins (ties go to Player 1).

## Features

- **Contract-Generated Dice**: Uses Soroban PRNG to generate dice values
- **Two-Player Games**: Each game involves exactly two players
- **Simple Rules**: Roll two dice each, highest total wins
- **Multiple Concurrent Games**: Support for multiple independent games
- **Game Hub Integration**: Uses `start_game` and `end_game` for points locking and results

## Contract Methods
# Dice Duel

`dice-duel` is a two-player Soroban game that uses commit/reveal so neither a
caller nor the contract can choose a winning public input after seeing the
other player's contribution.

## Protocol

Each player generates a uniformly random `BytesN<32>` secret off chain. A
commitment is SHA-256 over a canonical Soroban `SCV_MAP` XDR value containing
the protocol version and domain, this Dice Duel contract address, the
`dice-duel` game tag, session ID, player role and address, both player
addresses, both exact stakes, and the secret. A separate salt is unnecessary:
a uniformly random 256-bit secret already supplies the required entropy.

Both commitments, both complete player authorizations, and the reveal deadline
are supplied to `start_game` before the Hub locks either stake. Each player then
reveals with `roll`. Reveals are accepted through the deadline inclusively. The
four dice are derived in fixed player-role order from both verified secrets;
they do not depend on reveal order, ledger time, or `session_id`.

After both reveals, anyone may call `reveal_winner`. Ties go to player 1. After
the deadline, anyone may call `resolve_timeout`: exactly one revealer wins by
forfeit, while zero reveals cause a neutral Hub cancellation and exact refund
of each player's asymmetric stake. Terminal Hub interaction is guarded by the
`Finalizing` phase and Soroban transaction rollback, so a failed Hub call can
be retried without leaving a partially finalized game.

Game records use persistent storage with a 30-day live TTL. An unattended game
is therefore archived rather than deleted irreversibly. If it has archived,
restore the Dice game entry and required contract/Hub footprint before calling
`reveal_winner`, `resolve_timeout`, or `get_game`.

## ABI

### `commitment`

Computes the commitment for a player. Production clients should reproduce this
canonical encoding and hash locally, using the golden vector in the test suite,
and retain the secret securely until reveal. **Never sign or submit a
`commitment` invocation as a transaction, and never send the secret to an
untrusted RPC.** Trusted local simulation is suitable only as a development
cross-check.

```text
commitment(
session_id: u32,
role: u32, // 1 = player 1, 2 = player 2
revealing_player: Address,
player1: Address,
player2: Address,
player1_points: i128,
player2_points: i128,
secret: BytesN<32>,
) -> BytesN<32>
```

### `start_game`
Start a new game between two players.

**Parameters:**
- `session_id: u32`
- `player1: Address`
- `player2: Address`
- `player1_points: i128`
- `player2_points: i128`

**Returns:** `Result<(), Error>`
```text
start_game(
session_id: u32,
player1: Address,
player2: Address,
player1_points: i128,
player2_points: i128,
player1_commitment: BytesN<32>,
player2_commitment: BytesN<32>,
reveal_deadline: u32,
) -> Result<(), Error>
```

**Auth:** Requires authentication from both players
Both players authorize the complete argument set. The deadline must be in the
future and fit within the persistent entry's 30-day live TTL while preserving
the timeout-resolution grace window.

### `roll`
Commit a roll for the current game.

**Parameters:**
- `session_id: u32`
- `player: Address`

**Returns:** `Result<(), Error>`
```text
roll(session_id: u32, player: Address, secret: BytesN<32>)
-> Result<(), Error>
```

**Auth:** Requires authentication from the rolling player
Requires the revealing player's authorization. It rejects a wrong secret, a
duplicate reveal, and a reveal after the deadline.

### `reveal_winner`
Reveal the winner after both players have rolled.

**Parameters:**
- `session_id: u32`
```text
reveal_winner(session_id: u32) -> Result<Address, Error>
```

**Returns:** `Result<Address, Error>` - Address of the winning player
Permissionless. It settles a game only after both valid reveals.

**Note:** Can only be called after both players have rolled. If totals are equal,
Player 1 wins the tie.
### `resolve_timeout`

### `get_game`
Get the current state of a game.
```text
resolve_timeout(session_id: u32) -> Result<Option<Address>, Error>
```

**Parameters:**
- `session_id: u32`
Permissionless and available only after the deadline. Returns the sole
revealer for a forfeit or `None` for neutral cancellation.

**Returns:** `Result<Game, Error>` - The game state
### Queries and administration

## Game Flow
- `get_game(session_id)` returns the complete stored game state.
- `get_admin()` and `get_hub()` return current configuration.
- `set_admin(new_admin)`, `set_hub(new_hub)`, and `upgrade(new_wasm_hash)`
require administrator authorization.

1. Two players call `start_game` to create a new game
2. Each player calls `roll` to commit their roll
3. Once both players have rolled, anyone can call `reveal_winner`
4. The contract generates two dice for each player
5. The game is marked as ended and the winner is recorded
Each game snapshots its Hub address at creation. Changing the configured Hub
therefore affects only later games.

## Error Codes
## Errors

- `GameNotFound` (1): The specified session does not exist
- `NotPlayer` (2): Caller is not a player in this game
- `AlreadyRolled` (3): Player already committed their roll
- `BothPlayersNotRolled` (4): Cannot reveal winner until both players roll
- `GameAlreadyEnded` (5): Game already ended
Codes 1 through 5 are the legacy Dice Duel ABI and remain stable:

## Building
1. `GameNotFound`
2. `NotPlayer`
3. `AlreadyRolled`
4. `BothPlayersNotRolled`
5. `GameAlreadyEnded`

```bash
stellar contract build
```
New errors use codes above the preserved range:

Output: `target/wasm32v1-none/release/dice_duel.wasm`
6. `SamePlayer`
7. `GameAlreadyExists`
8. `InvalidDeadline`
9. `RevealDeadlinePassed`
10. `RevealDeadlineNotReached`
11. `WrongSecret`
12. `FinalizationInProgress`

## Testing
## Build and test

```bash
cargo test
cargo test --workspace --locked -j 2
cargo build --locked --release --target wasm32v1-none -p dice-duel -p mock-game-hub
cargo test -p dice-duel --test wasm_resource --locked -- --ignored --nocapture
```

## Technical Details
The ignored resource test loads the optimized Wasm files at runtime and checks
normal settlement, one-sided forfeit, and zero-reveal cancellation under the
Soroban SDK's mainnet invocation limits.

## Deployment gates

- **Deterministic PRNG**: Uses a deterministic seed so results are stable between
simulation and submission.
- **Storage**: Uses temporary storage with a 30-day TTL.
The repository mock demonstrates the proposed neutral `cancel_game(session_id)`
behavior; it is not evidence of production Hub settlement economics. Before
deployment, the production Hub must confirm that ABI and exact refund
semantics. Because the game state and public ABI changed, deploy a new Dice
contract ID (or prove all legacy sessions are drained), then regenerate Studio
bindings and add secure client-side secret retention and timeout UI. This
change does not update the Studio contract ID or generated TypeScript bindings.
Loading