diff --git a/examples/battleship/Cargo.toml b/examples/battleship/Cargo.toml index 5f4688d1..134d88d2 100644 --- a/examples/battleship/Cargo.toml +++ b/examples/battleship/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = "1.0.0" +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } diff --git a/examples/battleship/README.md b/examples/battleship/README.md index e09501de..d5665114 100644 --- a/examples/battleship/README.md +++ b/examples/battleship/README.md @@ -2,7 +2,11 @@ A two-player Battleship game demonstrating **hidden information** using commit-reveal pattern and Merkle proofs on Stellar Soroban. Players commit their board layouts cryptographically, then prove hit/miss results without revealing unattacked positions. -This example is Cougr's canonical hidden-information reference. It intentionally leans on the stable privacy surface in `cougr_core::privacy::stable` instead of re-defining Merkle verification inside the example. +This example is Cougr's **canonical** hidden-information reference. It intentionally leans on the stable privacy surface in `cougr_core::privacy::stable` instead of re-defining Merkle verification inside the example. + +## Status + +**Canonical** — maintained reference implementation for commit-reveal + selective disclosure on Soroban. Uses `cougr-core = "1.1.0"`, `privacy::stable` Merkle primitives, and `impl_component!` macros for standardized serialization. ## The Hidden Information Problem @@ -134,6 +138,55 @@ assert!(verifier.verify(&env, &proof, &merkle_root)?); The leaf payload still binds `index || value`, but the inclusion proof format and verification rules come from Cougr's stable privacy API. +## When to Use Commit-Reveal vs ZK Circuits + +| Pattern | Best For | Cost | Complexity | +|---------|----------|------|------------| +| **Commit-Reveal + Merkle** | Hidden boards, card hands, fog-of-war | O(log n) proof verification | Low — uses standard SHA256 | +| **ZK Circuits (Groth16/Poseidon)** | Private game logic evaluation, hidden card deals | Single on-chain verification | High — requires circuit compilation | + +Use **commit-reveal** when you need to hide state but reveal it incrementally with verifiable proofs. Use **ZK circuits** when the game logic itself must remain private (e.g., proving a move is valid without revealing the move). + +For a reference ZK implementation, see [hidden_hand](../hidden_hand/), which demonstrates circuit-based hidden card dealing with Groth16 proofs. + +## Storage Model + +### Committed State + +Stored on-chain during setup: +- `commitment_a/b` — SHA256 hash of each player's board with random salt +- `merkle_root_a/b` — root of the Merkle tree built from cell hashes +- `has_commitment_a/b` — flags tracking which players have committed + +Both commitments must be submitted before the game transitions to `Phase::Attack`. + +### Revealed State + +Updated during the attack phase: +- `attack_grid_a/b` — maps cell indices to `CellResult` (hit/miss) +- `ship_status` — tracks remaining ship cells per player (starts at 17) +- `turn_state` — tracks current player, phase, and pending reveals + +### Proven State + +The `reveal_cell` function verifies that disclosed cell values match the original commitment: +1. Constructs the expected leaf hash from `(index, value)` using the same SHA256 scheme as the commit phase +2. Validates the `OnChainMerkleProof` against the stored `merkle_root` using `Sha256MerkleProofVerifier` +3. Only if the proof verifies is the hit/miss recorded on-chain + +## Component Serialization + +`BoardCommitment` uses the `impl_component!` macro for standardized serialization: + +```rust +impl_component!(BoardCommitment, "board", Table, { + commitment: bytes32, + merkle_root: bytes32 +}); +``` + +This replaces manual byte-level serialization with a type-safe macro that handles big-endian encoding/decoding automatically. + ## Building & Testing ### Prerequisites @@ -151,7 +204,7 @@ cargo build --release --target wasm32v1-none cargo test ``` -**Test Coverage (10 tests):** +**Test Coverage (12 tests):** - ✅ Game initialization - ✅ Board commitment - ✅ Attack and reveal (miss) @@ -160,8 +213,10 @@ cargo test - ✅ Cannot attack same cell twice - ✅ Turn enforcement - ✅ Win condition -- ✅ Component trait serialization +- ✅ Component trait serialization (via `impl_component!`) - ✅ Turn switching +- ✅ Reveal without pending attack +- ✅ Attack before commit phase ## Example Usage @@ -266,6 +321,7 @@ stellar contract deploy \ - [Cougr Repository](https://github.com/salazarsebas/Cougr) - [Merkle Trees](https://en.wikipedia.org/wiki/Merkle_tree) - [Commitment Schemes](https://en.wikipedia.org/wiki/Commitment_scheme) +- [hidden_hand — ZK circuit example](../hidden_hand/) - [Soroban Documentation](https://developers.stellar.org/docs/build/smart-contracts) ## License diff --git a/examples/battleship/src/lib.rs b/examples/battleship/src/lib.rs index 9458123d..9ae3b2e0 100644 --- a/examples/battleship/src/lib.rs +++ b/examples/battleship/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use cougr_core::component::ComponentTrait; +use cougr_core::impl_component; use cougr_core::privacy::stable::{ MerkleProofVerifier, OnChainMerkleProof, Sha256MerkleProofVerifier, }; @@ -24,6 +24,10 @@ pub enum Phase { Finished, } +/// Commitment to a player's board layout. +/// +/// Both fields are 32-byte hashes: `commitment` binds the board to a salt, +/// and `merkle_root` enables selective cell-level proof verification. #[contracttype] #[derive(Clone, Debug)] pub struct BoardCommitment { @@ -31,26 +35,10 @@ pub struct BoardCommitment { pub merkle_root: BytesN<32>, } -impl ComponentTrait for BoardCommitment { - fn component_type() -> Symbol { - symbol_short!("board") - } - - fn serialize(&self, env: &Env) -> Bytes { - let mut bytes = Bytes::new(env); - for i in 0..32 { - bytes.push_back(self.commitment.get(i).unwrap()); - } - for i in 0..32 { - bytes.push_back(self.merkle_root.get(i).unwrap()); - } - bytes - } - - fn deserialize(_env: &Env, _data: &Bytes) -> Option { - None - } -} +impl_component!(BoardCommitment, "board", Table, { + commitment: bytes32, + merkle_root: bytes32 +}); #[contracttype] #[derive(Clone, Debug)] @@ -258,7 +246,7 @@ impl BattleshipContract { panic!("Not a player"); }; - // Verify Merkle proof + // Verify Merkle proof using privacy::stable primitives let coord = Self::coord_to_index(x, y); if proof.leaf_index != coord { panic!("Invalid proof"); diff --git a/examples/battleship/src/test.rs b/examples/battleship/src/test.rs index 2d387585..e8e66301 100644 --- a/examples/battleship/src/test.rs +++ b/examples/battleship/src/test.rs @@ -1,6 +1,6 @@ use super::*; use cougr_core::privacy::stable::{to_on_chain_proof, MerkleTree, OnChainMerkleProof}; -use soroban_sdk::{testutils::Address as _, Address, Bytes, BytesN, Env}; +use soroban_sdk::{testutils::Address as _, Address, Bytes, BytesN, Env, Vec}; fn setup_game() -> (Env, BattleshipContractClient<'static>, Address, Address) { let env = Env::default(); @@ -293,7 +293,7 @@ fn test_win_condition() { } #[test] -fn test_component_trait() { +fn test_component_trait_via_macro() { let env = Env::default(); let commitment = BoardCommitment { @@ -304,6 +304,11 @@ fn test_component_trait() { let serialized = commitment.serialize(&env); assert_eq!(serialized.len(), 64); assert_eq!(BoardCommitment::component_type(), symbol_short!("board")); + + // Test round-trip deserialize + let deserialized = BoardCommitment::deserialize(&env, &serialized).unwrap(); + assert_eq!(deserialized.commitment, commitment.commitment); + assert_eq!(deserialized.merkle_root, commitment.merkle_root); } #[test] @@ -347,3 +352,49 @@ fn test_turn_switching() { let state = client.get_state(); assert_eq!(state.turn_state.current_player, player_a); } + +#[test] +#[should_panic(expected = "No pending attack")] +fn test_reveal_without_pending_attack() { + let (env, client, player_a, player_b) = setup_game(); + env.mock_all_auths(); + + client.new_game(&player_a, &player_b); + + let board_a = [0u32; 100]; + let board_b = [0u32; 100]; + + let salt_a = BytesN::from_array(&env, &[1u8; 32]); + let salt_b = BytesN::from_array(&env, &[2u8; 32]); + + let commitment_a = make_commitment(&env, &board_a, &salt_a); + let commitment_b = make_commitment(&env, &board_b, &salt_b); + + let (root_a, _) = build_merkle_tree(&env, &board_a); + let (root_b, _) = build_merkle_tree(&env, &board_b); + + client.commit_board(&player_a, &commitment_a, &root_a); + client.commit_board(&player_b, &commitment_b, &root_b); + + // Try to reveal without attacking first + let fake_proof = OnChainMerkleProof { + siblings: Vec::new(&env), + path_bits: 0, + leaf: BytesN::from_array(&env, &[0u8; 32]), + leaf_index: 0, + depth: 0, + }; + client.reveal_cell(&player_b, &0, &0, &0, &fake_proof); +} + +#[test] +#[should_panic(expected = "Not in attack phase")] +fn test_attack_before_commit() { + let (env, client, player_a, _player_b) = setup_game(); + env.mock_all_auths(); + + client.new_game(&player_a, &Address::generate(&env)); + + // Try to attack before boards are committed + client.attack(&player_a, &0, &0); +} diff --git a/examples/proof_of_hunt/Cargo.toml b/examples/proof_of_hunt/Cargo.toml index 76d2995b..fe719488 100644 --- a/examples/proof_of_hunt/Cargo.toml +++ b/examples/proof_of_hunt/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" crate-type = ["cdylib", "rlib"] [dependencies] -cougr-core = "1.0.0" +cougr-core = "1.1.0" soroban-sdk = "25.1.0" [dev-dependencies] diff --git a/examples/proof_of_hunt/README.md b/examples/proof_of_hunt/README.md index 96e6b8d4..4827a71c 100644 --- a/examples/proof_of_hunt/README.md +++ b/examples/proof_of_hunt/README.md @@ -1,51 +1,136 @@ # Proof of Hunt (Stellar + Soroban) -Proof of Hunt is a hidden-map treasure discovery example for Soroban that combines: +A hidden-map treasure discovery game demonstrating **zero-knowledge proof-backed exploration** on Stellar Soroban. This example combines: -- hidden world state committed off-chain -- proof-backed exploration and deterministic progression on-chain -- premium actions modeled for x402 settlement flows +- Hidden world state committed off-chain via Merkle roots +- Proof-backed exploration using Groth16 verification and BN254 pairing checks +- Premium actions modeled for x402 settlement flows +- Nullifier-based replay prevention -This example is intentionally contract-only: no frontend, no multiplayer layer. +## Status + +**Transitional** — uses `cougr-core = "1.1.0"` and Stellar-zk style Groth16 verification. See [battleship](../battleship/) for the canonical commit-reveal reference, and [hidden_hand](../hidden_hand/) for the canonical ZK circuits reference. ## Why This Is Stellar-Specific This example demonstrates three Stellar-native patterns working together: -1. Soroban contract state for deterministic gameplay and progression. -2. stellar-zk style Groth16 verifier flow on-chain using Soroban BN254 pairing checks. -3. x402-style premium action credits represented as settled payment units before hint consumption. +1. Soroban contract state for deterministic gameplay and progression +2. Stellar-zk style Groth16 verifier flow on-chain using Soroban BN254 pairing checks +3. x402-style premium action credits represented as settled payment units before hint consumption References: - - https://crates.io/crates/stellar-zk - https://github.com/salazarsebas/stellar-zk - https://developers.stellar.org/docs/build/apps/x402 -## Hidden State And Proof Flow +## When to Use Commit-Reveal vs ZK Circuits + +Use **commit-reveal with Merkle proofs** (e.g., [battleship](../battleship/), [treasure_hunt](../treasure_hunt/)) when you need to hide state and reveal it incrementally with verifiable proofs. This is simpler and sufficient for most hidden-information games. + +Use **ZK circuits** (this example and [hidden_hand](../hidden_hand/)) when: +- The game logic itself must remain private (e.g., proving a valid move without revealing it) +- You need constant-size proofs regardless of map size +- You want nullifier-based replay prevention without exposing cell indices + +## Hidden State and Proof Flow ### Off-chain committed data The hidden map is represented off-chain and committed by root hash: - -- map commitment root (`BytesN<32>`) -- map dimensions (`width`, `height`) -- implicit treasure distribution encoded in proof public inputs +- Map commitment root (`BytesN<32>`) +- Map dimensions (`width`, `height`) +- Implicit treasure distribution encoded in proof public inputs ### What is proven on-chain For each exploration: - - `(x, y)` belongs to a valid proof statement bound to the same commitment root -- leaf + sibling path resolves to the committed root +- Leaf + sibling path resolves to the committed root - Groth16 proof verifies through BN254 pairing checks -- nullifier has not been replayed +- Nullifier has not been replayed ### Anti-cheat and privacy properties -- Players cannot claim arbitrary discoveries because proof public inputs are tied to coordinates and root. -- Replay is blocked by nullifier storage. -- Full hidden map remains off-chain; only commitment and selective proof metadata are revealed. +- Players cannot claim arbitrary discoveries because proof public inputs are tied to coordinates and root +- Replay is blocked by nullifier storage +- Full hidden map remains off-chain; only commitment and selective proof metadata are revealed + +## Contract API + +### Public Functions + +| Function | Parameters | Description | +|----------|-----------|-------------| +| `init_game` | `player: Address`, `map_commitment: BytesN<32>`, `width: u32`, `height: u32` | Initialize game with committed map | +| `explore` | `player: Address`, `x: u32`, `y: u32`, `proof: ProofInput` | Explore a cell with ZK proof | +| `purchase_hint` | `player: Address`, `hint_type: u32` | Buy hint (0) or scan (1) using credits | +| `get_state` | — | Return `GameState` | +| `is_finished` | — | Check if game is won or lost | +| `set_verification_key` | `owner: Address`, `vk_bytes: Bytes` | Set Groth16 verification key | +| `credit_x402_payment` | `owner: Address`, `player: Address`, `units: u32`, `receipt_hash: BytesN<32>` | Credit x402 payment units | + +### Data Types + +```rust +enum GameStatus { Active, Won, Lost } + +struct ProofInput { + proof: BytesN<256>, + public_inputs: Bytes, + nullifier: BytesN<32>, + leaf_hash: BytesN<32>, + sibling_hash: BytesN<32>, + sibling_on_left: bool, +} + +struct PlayerState { + position_x: u32, + position_y: u32, + score: i128, + health: u32, + discoveries: u32, +} + +struct GameState { + player: Address, + map_commitment: BytesN<32>, + width: u32, + height: u32, + treasure_count: u32, + discovered_cells: u32, + status: GameStatus, + player_state: PlayerState, + hint_usage: HintUsage, + x402_credits: u32, +} +``` + +## Storage Model + +### Committed State + +Stored on-chain during initialization: +- `map_commitment` — Merkle root of the hidden map +- Map dimensions (`width`, `height`) +- Derived `treasure_count = max(1, (width * height) / 8)` +- Verification key for Groth16 proofs + +### Revealed State + +Updated during exploration: +- `player_state` — position, score, health, discoveries +- `discovered_cells` — count of explored cells +- `explored_cell(cell_idx)` — persistent storage for replay prevention +- Game status (`Active`/`Won`/`Lost`) + +### Proven State + +The `explore` function performs multi-layer verification: +1. **Public input validation**: Checks that `(x, y)` in proof inputs match claimed coordinates +2. **Merkle path verification**: Reconstructs root from leaf + sibling path +3. **Groth16 verification**: Verifies BN254 pairing check via `env.crypto().bn254().pairing_check()` +4. **Nullifier check**: Rejects if nullifier was already used (replay prevention) ## x402 Premium Action Model @@ -56,55 +141,65 @@ This maps to an x402 backend flow where a payment gateway verifies and settles p - `hint_type = 0`: hint action (cost 1 credit) - `hint_type = 1`: scan action (cost 2 credits) -## Contract API - -Required functions: - -- `init_game(env, player, map_commitment, width, height)` -- `explore(env, player, x, y, proof)` -- `purchase_hint(env, player, hint_type)` -- `get_state(env) -> GameState` -- `is_finished(env) -> bool` +## Building & Testing -Additional helper functions: +### Prerequisites +- Rust 1.70.0+ +- Stellar CLI 25.0.0+ (optional) -- `set_verification_key(env, owner, vk_bytes)` -- `credit_x402_payment(env, owner, player, units, receipt_hash)` +### Build +```bash +cargo build +cargo build --release --target wasm32v1-none +``` -## Architecture Components +### Test +```bash +cargo test +``` -- `MapCommitmentComponent`: commitment root, width, height, derived treasure count -- `PlayerStateComponent`: position, score, health, discoveries -- `ExplorationComponent`: explored cell tracking -- `HintUsageComponent`: hints/scans used -- `GameStatusComponent`: active/won/lost +**Test Coverage:** +- ✅ Game initialization +- ✅ Valid exploration proof acceptance +- ✅ Invalid proof rejection +- ✅ Progression updates after valid exploration +- ✅ Premium hint action flow (x402 credits) +- ✅ Game completion (win condition) +- ✅ Failure condition (health depletion) +- ✅ Nullifier replay prevention +- ✅ Coordinate bounds validation +- ✅ Timeout and claim logic -### Systems +## Security Considerations -- `ExplorationSystem`: coordinate bounds + replay prevention -- `ProofValidationSystem`: public input checks + Merkle path + Groth16 verify -- `DiscoveryResolutionSystem`: score/health/discovery updates -- `HintPurchaseSystem`: x402 credit consumption -- `EndConditionSystem`: won/lost transitions +### ✅ Secure +- **Proof-backed exploration**: Cannot claim arbitrary discoveries without valid Groth16 proof +- **Nullifier replay prevention**: Each cell can only be explored once +- **BN254 pairing verification**: Cryptographic proof of correct computation +- **Public input binding**: Coordinates are bound to proof statement -## Build And Test +### ⚠️ Important +- **Zero-proof test path**: CI tests use deterministic zero-proof path (`#[cfg(test)]`) — production requires real Groth16 proofs +- **Verification key**: Must be set correctly before exploration; invalid VK causes verification failures -All commands are Soroban target aligned (`wasm32v1-none`): +## Deployment ```bash -cd examples/proof_of_hunt -cargo fmt --check -cargo clippy --all-targets --all-features -- -D warnings -cargo test -stellar contract build +stellar keys generate proof-of-hunt-deployer --network testnet --fund +stellar contract deploy \ + --wasm target/wasm32v1-none/release/proof_of_hunt.wasm \ + --source proof-of-hunt-deployer \ + --network testnet ``` -## Notes On stellar-zk Integration +## Resources -The contract uses the same Groth16 verifier model used by stellar-zk templates: +- [Cougr Repository](https://github.com/salazarsebas/Cougr) +- [stellar-zk](https://github.com/salazarsebas/stellar-zk) +- [battleship — canonical commit-reveal example](../battleship/) +- [hidden_hand — canonical ZK circuit example](../hidden_hand/) +- [Soroban BN254 Documentation](https://developers.stellar.org/docs/build/smart-contracts) -- BN254 proof point decoding -- verification key layout parsing -- multi-pairing check (`env.crypto().bn254().pairing_check`) +## License -For CI tests in this repository, a deterministic zero-proof test path is enabled only under `#[cfg(test)]` so tests remain fully deterministic without external trusted setup artifacts. +MIT OR Apache-2.0 diff --git a/examples/rock_paper_scissors/Cargo.toml b/examples/rock_paper_scissors/Cargo.toml index 6849ee0a..a933d199 100644 --- a/examples/rock_paper_scissors/Cargo.toml +++ b/examples/rock_paper_scissors/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = "1.0.0" +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } diff --git a/examples/rock_paper_scissors/README.md b/examples/rock_paper_scissors/README.md index 172473d7..cc8f8085 100644 --- a/examples/rock_paper_scissors/README.md +++ b/examples/rock_paper_scissors/README.md @@ -1,6 +1,10 @@ # Rock Paper Scissors with Commit-Reveal -A two-player Rock Paper Scissors game demonstrating the **commit-reveal pattern** using cryptographic hashing on Stellar Soroban. This is the simplest example of zero-knowledge cryptography in the Cougr framework. +A two-player Rock Paper Scissors game demonstrating the **commit-reveal pattern** using cryptographic hashing on Stellar Soroban. This is the simplest example of hidden information in the Cougr framework. + +## Status + +**Transitional** — uses `cougr-core = "1.1.0"` with partial migration to `impl_component!` macros. See [battleship](../battleship/) for the canonical hidden-information reference. ## What is Commit-Reveal? @@ -33,6 +37,12 @@ REVEAL PHASE (prove choices) - ✅ **Hiding**: Opponent can't see choice until reveal - ✅ **Order-independent**: Neither player gains advantage by going first/second +## When to Use Commit-Reveal vs ZK Circuits + +Use **commit-reveal** (this pattern) when you need to hide simple choices that are later revealed and verified by recomputing a hash. It's simple, efficient, and doesn't require external proving systems. + +Use **ZK circuits** when the game logic itself must remain private — for example, proving that a move is valid without revealing what the move is. See [hidden_hand](../hidden_hand/) for a canonical ZK circuit-based example using Groth16 proofs. + ## Game Flow ### 1. Initialize Match @@ -69,7 +79,7 @@ When both players reveal → automatically resolves round ### 4. Resolution ``` Rock > Scissors -Scissors > Paper +Scissors > Paper Paper > Rock Same choice = Draw ``` @@ -87,46 +97,29 @@ After 100 ledgers, the honest player who revealed wins by forfeit. ## Contract API +### Public Functions + | Function | Parameters | Description | |----------|-----------|-------------| -| `new_match` | `player_a: Address`
`player_b: Address`
`best_of: u32` | Initialize new match | -| `commit` | `player: Address`
`hash: BytesN<32>` | Submit commitment hash | -| `reveal` | `player: Address`
`choice: u32`
`salt: BytesN<32>` | Reveal choice (0=Rock, 1=Paper, 2=Scissors) | -| `claim_timeout` | `player: Address` | Claim win if opponent doesn't reveal | -| `get_state` | - | Get current match state | -| `get_score` | - | Get scoreboard | +| `new_match` | `player_a: Address`, `player_b: Address`, `best_of: u32` | Initialize a new match | +| `commit` | `player: Address`, `hash: BytesN<32>` | Submit commitment hash | +| `reveal` | `player: Address`, `choice: u32`, `salt: BytesN<32>` | Reveal choice (0=Rock, 1=Paper, 2=Scissors) | +| `claim_timeout` | `player: Address` | Claim win if opponent doesn't reveal after 100 ledgers | +| `get_state` | — | Get current `MatchState` | +| `get_score` | — | Get `ScoreBoard` | -## Data Structures +### Data Types -### Choice ```rust -enum Choice { - Rock = 0, - Paper = 1, - Scissors = 2, -} -``` +enum Choice { Rock = 0, Paper = 1, Scissors = 2 } +enum Phase { Committing, Revealing, Resolved } -### Phase -```rust -enum Phase { - Committing, // Waiting for both commitments - Revealing, // Waiting for both reveals - Resolved, // Match complete -} -``` - -### MatchState -```rust struct MatchState { phase: Phase, winner: Option
, round: u32, } -``` -### ScoreBoard -```rust struct ScoreBoard { wins_a: u32, wins_b: u32, @@ -135,22 +128,50 @@ struct ScoreBoard { } ``` +## Storage Model + +### Committed State + +Stored on-chain during the commit phase: +- `hash_a/b` — SHA256 hash of each player's choice + random salt +- `has_commit_a/b` — flags tracking which players have committed +- `commit_ledger` — ledger sequence when both players committed (for timeout calculation) + +Both commitments must be submitted before the game transitions to `Phase::Revealing`. + +### Revealed State + +Updated during the reveal phase: +- `revealed_a/b` — flags tracking which players have revealed +- `choice_a/b` — actual numeric choices (0, 1, or 2) +- `scoreboard` — accumulated wins/draws across rounds + +### Proven State + +The `reveal` function verifies that disclosed choices match the original commitment by recomputing `SHA256(choice || salt)` and comparing it to the stored hash. + +## Component Serialization + +`PlayerCommitment` uses the `impl_component!` macro for standardized serialization: + +```rust +impl_component!(PlayerCommitment, "commit", Table, { + hash: bytes32, + revealed: bool +}); +``` + +`MatchState` implements `ComponentTrait` manually since its `Phase` enum and `Option
` fields don't map cleanly to the macro's fixed-size type system. + ## Building & Testing ### Prerequisites - Rust 1.70.0+ - Stellar CLI 25.0.0+ (optional) -```bash -cargo install stellar-cli -``` - ### Build ```bash -# Development build cargo build - -# Optimized WASM cargo build --release --target wasm32v1-none ``` @@ -159,7 +180,7 @@ cargo build --release --target wasm32v1-none cargo test ``` -**Test Coverage (15 tests):** +**Test Coverage:** - ✅ Match initialization - ✅ Commit phase transitions - ✅ All 9 choice combinations (RR, RP, RS, PR, PP, PS, SR, SP, SS) @@ -167,65 +188,7 @@ cargo test - ✅ Best-of-3 match flow - ✅ Double commit prevention - ✅ Premature reveal prevention -- ✅ Component trait serialization - -## Example Usage - -### Off-Chain (Player) -```rust -use soroban_sdk::{Bytes, BytesN, Env}; - -// Generate random salt -let salt = BytesN::from_array(&env, &[42u8; 32]); - -// Choose Rock (0) -let choice = 0u32; - -// Compute commitment hash -let mut data = Bytes::new(&env); -data.append(&Bytes::from_array(&env, &choice.to_be_bytes())); -for i in 0..32 { - data.push_back(salt.get(i).unwrap()); -} -let hash = env.crypto().sha256(&data); - -// Submit commitment -client.commit(&player, &hash.into()); - -// Later, reveal -client.reveal(&player, &choice, &salt); -``` - -### On-Chain (Contract) -```rust -// Verify hash matches -let computed = sha256(choice || salt); -if computed != stored_hash { - panic!("Hash mismatch"); -} - -// Resolve round -if choice_a.beats(choice_b) { - wins_a += 1; -} -``` - -## Why SHA256 Instead of Poseidon2? - -This example uses SHA256 for simplicity and immediate usability. Poseidon2 is a ZK-friendly hash function that's more efficient in zero-knowledge circuits (~300 constraints vs SHA256's ~28,000), but requires the `hazmat-crypto` feature flag. - -**For production ZK applications**, use Poseidon2: -```rust -use cougr_core::privacy::experimental::poseidon2_hash; - -let hash = poseidon2_hash(&env, ¶ms, &choice_u256, &salt_u256); -``` - -**For this educational example**, SHA256 is: -- ✅ Built into Soroban SDK -- ✅ No feature flags needed -- ✅ Demonstrates commit-reveal pattern clearly -- ✅ Cryptographically secure for this use case +- ✅ Component trait serialization (via `impl_component!`) ## Security Considerations @@ -238,130 +201,24 @@ let hash = poseidon2_hash(&env, ¶ms, &choice_u256, &salt_u256); ### ⚠️ Important - **Salt randomness**: Use cryptographically secure random salts (32 bytes) - **Salt uniqueness**: Never reuse salts across rounds -- **Timeout value**: 100 ledgers (~8 minutes on Stellar) - adjust for your needs - -### 🔒 Best Practices -```rust -// ✅ Good: Random salt per round -let salt = generate_random_bytes(32); - -// ❌ Bad: Predictable salt -let salt = BytesN::from_array(&env, &[0u8; 32]); - -// ❌ Bad: Reused salt -let salt = player_address.to_bytes(); -``` - -## ECS Architecture - -### Components - -| Component | Fields | Purpose | -|-----------|--------|---------| -| `PlayerCommitment` | `hash: BytesN<32>`
`revealed: bool` | Stores commitment hash | -| `MatchState` | `phase: Phase`
`winner: Option
`
`round: u32` | Game phase tracking | -| `ScoreBoard` | `wins_a: u32`
`wins_b: u32`
`draws: u32`
`best_of: u32` | Match scoring | - -All components implement `cougr_core::component::ComponentTrait` for type-safe serialization. - -### Systems - -| System | Responsibility | -|--------|---------------| -| **CommitSystem** | Accepts hashes, transitions to reveal when both committed | -| **RevealSystem** | Verifies `sha256(choice || salt) == hash`, rejects mismatches | -| **ResolveSystem** | Compares choices, updates scoreboard | -| **MatchSystem** | Checks best-of-N threshold, declares winner or starts next round | ## Deployment -### Deploy to Testnet ```bash -# Generate funded account stellar keys generate rps-deployer --network testnet --fund - -# Build contract -cargo build --release --target wasm32v1-none - -# Deploy stellar contract deploy \ --wasm target/wasm32v1-none/release/rock_paper_scissors.wasm \ --source rps-deployer \ --network testnet ``` -### Play a Game -```bash -CONTRACT_ID= - -# Initialize match -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - -- new_match \ - --player_a \ - --player_b \ - --best_of 3 - -# Player A commits (compute hash off-chain first) -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - --source player-a \ - -- commit \ - --player \ - --hash - -# Player B commits -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - --source player-b \ - -- commit \ - --player \ - --hash - -# Player A reveals -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - --source player-a \ - -- reveal \ - --player \ - --choice 0 \ - --salt - -# Player B reveals -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - --source player-b \ - -- reveal \ - --player \ - --choice 1 \ - --salt - -# Check results -stellar contract invoke \ - --id $CONTRACT_ID \ - --network testnet \ - -- get_score -``` - -## Learning Path - -This example is the **entry point** for understanding Cougr's cryptographic primitives: - -1. **Start here**: Commit-reveal with SHA256 (this example) -2. **Next**: Upgrade to Poseidon2 hashing (ZK-friendly) -3. **Advanced**: Full ZK proofs with circuits (see `examples/chess/`) - ## Resources - [Cougr Repository](https://github.com/salazarsebas/Cougr) - [Commit-Reveal Schemes](https://en.wikipedia.org/wiki/Commitment_scheme) +- [battleship — canonical commit-reveal example](../battleship/) +- [hidden_hand — ZK circuit example](../hidden_hand/) - [Soroban Documentation](https://developers.stellar.org/docs/build/smart-contracts) -- [Poseidon Hash Function](https://www.poseidon-hash.info/) ## License diff --git a/examples/rock_paper_scissors/src/lib.rs b/examples/rock_paper_scissors/src/lib.rs index 8684ccc0..74fff480 100644 --- a/examples/rock_paper_scissors/src/lib.rs +++ b/examples/rock_paper_scissors/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use cougr_core::component::ComponentTrait; +use cougr_core::impl_component; use soroban_sdk::{ contract, contractimpl, contracttype, symbol_short, Address, Bytes, BytesN, Env, Symbol, }; @@ -33,6 +33,8 @@ impl Choice { } } +/// Player commitment storing the choice hash and reveal status. +/// Uses `impl_component!` for standardized serialization instead of manual byte encoding. #[contracttype] #[derive(Clone, Debug)] pub struct PlayerCommitment { @@ -40,24 +42,10 @@ pub struct PlayerCommitment { pub revealed: bool, } -impl ComponentTrait for PlayerCommitment { - fn component_type() -> Symbol { - symbol_short!("commit") - } - - fn serialize(&self, env: &Env) -> Bytes { - let mut bytes = Bytes::new(env); - for i in 0..32 { - bytes.push_back(self.hash.get(i).unwrap()); - } - bytes.push_back(if self.revealed { 1 } else { 0 }); - bytes - } - - fn deserialize(_env: &Env, _data: &Bytes) -> Option { - None - } -} +impl_component!(PlayerCommitment, "commit", Table, { + hash: bytes32, + revealed: bool +}); #[contracttype] #[derive(Clone, Debug, PartialEq, Eq)] @@ -75,13 +63,19 @@ pub struct MatchState { pub round: u32, } -impl ComponentTrait for MatchState { +impl cougr_core::component::ComponentTrait for MatchState { fn component_type() -> Symbol { symbol_short!("match") } fn serialize(&self, env: &Env) -> Bytes { let mut bytes = Bytes::new(env); + let phase_byte = match self.phase { + Phase::Committing => 0u8, + Phase::Revealing => 1u8, + Phase::Resolved => 2u8, + }; + bytes.append(&Bytes::from_array(env, &[phase_byte])); bytes.append(&Bytes::from_array(env, &self.round.to_be_bytes())); bytes } diff --git a/examples/treasure_hunt/Cargo.toml b/examples/treasure_hunt/Cargo.toml index 238d1c6f..4d18a919 100644 --- a/examples/treasure_hunt/Cargo.toml +++ b/examples/treasure_hunt/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = "1.0.0" +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } diff --git a/examples/treasure_hunt/README.md b/examples/treasure_hunt/README.md index bd75cf23..68d5ad6f 100644 --- a/examples/treasure_hunt/README.md +++ b/examples/treasure_hunt/README.md @@ -1,86 +1,178 @@ # Treasure Hunt (Merkle Map Pattern) -`treasure_hunt` demonstrates a core on-chain game pattern: +A single-player exploration game demonstrating **hidden information** using Merkle map commitments and sparse fog-of-war on Stellar Soroban. The full map is committed off-chain, and players prove cell contents via Merkle inclusion proofs as they explore. -1. Build a large map off-chain. -2. Store only a Merkle root on-chain. -3. Require Merkle proofs to reveal cell contents. -4. Track revealed cells as sparse state ("fog-of-war"). +## Status -## Why this pattern matters +**Transitional** — uses `cougr-core = "1.1.0"` and `privacy::stable` Merkle primitives (`MerkleTree`, `SparseMerkleTree`, `verify_inclusion`, `OnChainMerkleProof`). See [battleship](../battleship/) for the canonical hidden-information reference. -Large maps are too expensive to store fully on-chain. This example keeps -on-chain storage bounded: +## The Hidden Information Pattern -- Full map remains off-chain. -- Contract stores only: - - committed map root - - player/game state - - explored-cell sparse state +Large maps are too expensive to store fully on-chain. This example keeps on-chain storage bounded: -This enables large worlds while preserving verifiability. +- Full map remains off-chain, committed as a Merkle root +- Contract stores only: committed root, player state, and explored-cell sparse state +- Players reveal cells one at a time using Merkle inclusion proofs -## Cell encoding and map commitment +### Commit Phase (Off-Chain) -Each map cell is encoded into 32 bytes: +``` +Map Generator: +├─ Encode each cell as 32-byte leaf: [x(4) | y(4) | value(1) | padding(23)] +├─ Build SHA256 Merkle tree from all leaves +├─ Store root hash on-chain via init_game() +└─ Keep full tree off-chain for proof generation +``` + +### Reveal Phase (On-Chain) + +``` +Player: +├─ Moves to adjacent cell (x, y) +├─ Submits: cell_value + MerkleProof(siblings, leaf_hash, leaf_index) +└─ Contract: + ├─ verify_inclusion(proof, committed_root) → true/false + ├─ Applies discovery (treasure → +score, trap → -health) + └─ Updates sparse fog-of-war via SparseMerkleTree +``` -- bytes `[0..4]`: `x` (big-endian) -- bytes `[4..8]`: `y` (big-endian) -- byte `[8]`: `cell_value` (`0=empty`, `1=treasure`, `2=trap`) -- remaining bytes: zero +## When to Use Commit-Reveal vs ZK Circuits -The off-chain generator: +Use **commit-reveal with Merkle proofs** (this pattern) when you need to hide large state spaces (maps, card decks, board layouts) and reveal them incrementally. Merkle proofs provide O(log n) verification without exposing the full state. -- builds all encoded leaves in row-major order (`idx = y * width + x`) -- builds a SHA256 Merkle tree with `cougr_core::zk::merkle::MerkleTree` -- publishes the root and map dimensions via `init_game` +Use **ZK circuits** when the game logic itself must remain private — proving validity of moves without revealing any state. See [hidden_hand](../hidden_hand/) for a canonical ZK circuit example, and [proof_of_hunt](../proof_of_hunt/) for Groth16-based hidden state. -## Contract flow +## Contract API -### `init_game(player, map_root, width, height, total_treasures)` +### Public Functions -- stores map commitment metadata -- initializes player at `(0,0)`, health/score defaults -- initializes empty fog-of-war root from `SparseMerkleTree` +| Function | Parameters | Description | +|----------|-----------|-------------| +| `init_game` | `player: Address`, `map_root: BytesN<32>`, `width: u32`, `height: u32`, `total_treasures: u32` | Initialize game with committed map root | +| `explore` | `player: Address`, `x: u32`, `y: u32`, `cell_value: u32`, `proof: Vec>` | Explore a cell with Merkle proof | +| `get_state` | — | Return complete `GameState` | +| `is_explored` | `x: u32`, `y: u32` | Check if a cell has been revealed | -### `explore(player, x, y, cell_value, proof)` +### Data Types -- checks auth and active game -- enforces adjacent movement and bounds -- rejects already explored cells -- reconstructs Merkle proof structure and verifies inclusion against committed root -- applies discovery effects: - - treasure: +score, +treasures_found - - trap: health deduction -- marks cell explored -- recomputes sparse fog root using `SparseMerkleTree` -- updates game status (`Won`/`Lost`/`Active`) +```rust +enum GameStatus { Active, Won, Lost } -### `get_state()` +struct MapRoot { + root: BytesN<32>, // Merkle root of full map + width: u32, + height: u32, + total_treasures: u32, +} -Returns complete game state including map root metadata, player stats, -explored map, game config, and current fog root. +struct PlayerState { + x: u32, y: u32, health: u32, score: u32, treasures_found: u32 +} + +struct ExploredMap { + explored: Map // Sparse fog-of-war tracking +} +``` -### `is_explored(x, y)` +## Storage Model -Returns whether the cell has been revealed. +### Committed State -## Build and test +Stored on-chain during initialization: +- `map_root` — SHA256 Merkle root of the full encoded map +- Map dimensions (`width`, `height`) and `total_treasures` +- `fog_root` — initial sparse Merkle tree root (all cells unexplored) +### Revealed State + +Updated during exploration: +- `explored_map.explored` — sparse map of visited cell indices +- `player_state` — position, health, score, treasures found +- `fog_root` — recomputed `SparseMerkleTree` root after each exploration + +### Proven State + +The `explore` function verifies cell contents via Merkle inclusion: +1. Reconstructs the leaf hash from `(x, y, cell_value)` using the same encoding as the commit phase +2. Builds `OnChainMerkleProof` from sibling hashes and path bits +3. Calls `verify_inclusion(&env, &proof, &map_root.root)` — rejects if proof is invalid +4. Only if verification passes is the discovery applied and the cell marked explored + +## Cell Encoding + +Each map cell is encoded into a 32-byte leaf: + +``` +bytes [0..4]: x coordinate (big-endian u32) +bytes [4..8]: y coordinate (big-endian u32) +byte [8]: cell_value (0=empty, 1=treasure, 2=trap) +bytes [9..31]: zero padding +``` + +The off-chain generator hashes each cell and builds a SHA256 Merkle tree using `MerkleTree::from_leaves()` from `privacy::stable`. + +## Building & Testing + +### Prerequisites +- Rust 1.70.0+ +- Stellar CLI 25.0.0+ (optional) + +### Build ```bash -cd examples/treasure_hunt cargo build -stellar contract build +cargo build --release --target wasm32v1-none +``` + +### Test +```bash cargo test ``` -## Test coverage highlights +**Test Coverage:** +- ✅ Game initialization with committed map root +- ✅ Valid exploration with correct Merkle proof +- ✅ Invalid proof rejection +- ✅ Treasure scoring and trap damage +- ✅ Re-exploration rejection +- ✅ Win condition (all treasures found) +- ✅ Loss condition (health reaches zero) +- ✅ Full playable sequence from init to terminal state +- ✅ Sparse fog root updates after exploration +- ✅ Non-adjacent move rejection +- ✅ Commit phase validation +- ✅ Reveal phase with valid proof +- ✅ Invalid reveal value rejection + +## Security Considerations + +### ✅ Secure +- **Map commitment binding**: Merkle root prevents changing cell contents after init +- **Selective reveal**: Only explored cells are revealed; unexplored cells stay hidden +- **Proof verification**: Invalid proofs are rejected by `verify_inclusion()` +- **Adjacency enforcement**: Players can only move to adjacent cells + +### ⚠️ Important +- **Off-chain map trust**: The map must be generated honestly; the contract trusts the committed root +- **Sparse fog-of-war**: Recomputed on every move for full explored-cell tracking + +## Deployment + +```bash +stellar keys generate treasure-hunt-deployer --network testnet --fund +stellar contract deploy \ + --wasm target/wasm32v1-none/release/treasure_hunt.wasm \ + --source treasure-hunt-deployer \ + --network testnet +``` + +## Resources + +- [Cougr Repository](https://github.com/salazarsebas/Cougr) +- [Merkle Trees](https://en.wikipedia.org/wiki/Merkle_tree) +- [battleship — canonical commit-reveal example](../battleship/) +- [hidden_hand — ZK circuit example](../hidden_hand/) +- [Soroban Documentation](https://developers.stellar.org/docs/build/smart-contracts) + +## License -- valid exploration with correct Merkle proof -- invalid proof rejection -- treasure scoring and trap damage -- re-exploration rejection -- win condition (all treasures found) -- loss condition (health reaches zero) -- full playable sequence from init to terminal state -- sparse fog root updates after exploration +MIT OR Apache-2.0 diff --git a/examples/treasure_hunt/src/lib.rs b/examples/treasure_hunt/src/lib.rs index 33338118..a37e0cd1 100644 --- a/examples/treasure_hunt/src/lib.rs +++ b/examples/treasure_hunt/src/lib.rs @@ -1,9 +1,11 @@ #![no_std] -use cougr_core::zk::{verify_inclusion, OnChainMerkleProof, SparseMerkleTree}; +use cougr_core::privacy::stable::{ + MerkleProofVerifier, OnChainMerkleProof, Sha256MerkleProofVerifier, SparseMerkleTree, +}; use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env, - Map, Vec, + contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Bytes, BytesN, + Env, Map, }; #[cfg(test)] @@ -163,7 +165,7 @@ impl TreasureHuntContract { x: u32, y: u32, cell_value: u32, - proof: Vec>, + proof: OnChainMerkleProof, ) { ensure_initialized(&env); ensure_active(&env); @@ -183,8 +185,14 @@ impl TreasureHuntContract { panic_with_error!(&env, TreasureHuntError::AlreadyExplored); } - let on_chain_proof = make_on_chain_proof(&env, &proof, x, y, cell_value_u8, map_root.width); - let is_valid = verify_inclusion(&env, &on_chain_proof, &map_root.root) + let expected_leaf = hash_leaf_for_proof(&env, x, y, cell_value_u8); + if proof.leaf_index != idx || proof.leaf != expected_leaf { + panic_with_error!(&env, TreasureHuntError::InvalidProof); + } + + let verifier = Sha256MerkleProofVerifier; + let is_valid = verifier + .verify(&env, &proof, &map_root.root) .unwrap_or_else(|_| panic_with_error!(&env, TreasureHuntError::InvalidProof)); if !is_valid { panic_with_error!(&env, TreasureHuntError::InvalidProof); @@ -345,37 +353,11 @@ fn hash_leaf_for_proof(env: &Env, x: u32, y: u32, cell_value: u8) -> BytesN<32> inbuf[1..].copy_from_slice(&raw); let hash = env .crypto() - .sha256(&soroban_sdk::Bytes::from_slice(env, &inbuf)) + .sha256(&Bytes::from_slice(env, &inbuf)) .to_array(); BytesN::from_array(env, &hash) } -fn make_on_chain_proof( - env: &Env, - siblings: &Vec>, - x: u32, - y: u32, - cell_value: u8, - width: u32, -) -> OnChainMerkleProof { - let leaf_index = cell_index(x, y, width); - let depth = siblings.len(); - let mut path_bits: u32 = 0; - for i in 0..depth { - let is_right = ((leaf_index >> i) & 1) == 1; - if is_right { - path_bits |= 1 << i; - } - } - OnChainMerkleProof { - siblings: siblings.clone(), - path_bits, - leaf: hash_leaf_for_proof(env, x, y, cell_value), - leaf_index, - depth, - } -} - fn apply_discovery( player_state: &mut PlayerState, game_config: &mut GameConfig, diff --git a/examples/treasure_hunt/src/test.rs b/examples/treasure_hunt/src/test.rs index c8bea80e..f3737bf4 100644 --- a/examples/treasure_hunt/src/test.rs +++ b/examples/treasure_hunt/src/test.rs @@ -1,8 +1,8 @@ use super::*; -use cougr_core::zk::MerkleTree; +use cougr_core::privacy::stable::{to_on_chain_proof, MerkleTree, OnChainMerkleProof}; extern crate alloc; use alloc::vec::Vec as StdVec; -use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, Vec}; +use soroban_sdk::{testutils::Address as _, Address, Env}; const MAP_W: u32 = 3; const MAP_H: u32 = 3; @@ -54,14 +54,10 @@ fn idx(x: u32, y: u32) -> u32 { y * MAP_W + x } -fn proof_vec(env: &Env, map: &OffchainMap, x: u32, y: u32) -> Vec> { +fn proof_for(env: &Env, map: &OffchainMap, x: u32, y: u32) -> OnChainMerkleProof { let leaf_idx = idx(x, y); let proof = map.tree.proof(leaf_idx).unwrap(); - let mut siblings: Vec> = Vec::new(env); - for sibling in proof.siblings { - siblings.push_back(BytesN::from_array(env, &sibling)); - } - siblings + to_on_chain_proof(&proof, env) } fn value_at(map: &OffchainMap, x: u32, y: u32) -> u32 { @@ -99,7 +95,7 @@ fn test_valid_exploration_with_correct_proof() { let x = 1u32; let y = 0u32; let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); let state = client.get_state(); @@ -127,7 +123,7 @@ fn test_invalid_proof_rejected() { } else { CELL_TREASURE as u32 }; - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &bad_value, &proof); } @@ -154,7 +150,7 @@ fn test_trap_damage_and_loss_condition() { break; } let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); } @@ -175,7 +171,7 @@ fn test_reexplore_rejected() { let x = 1u32; let y = 0u32; let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); client.explore(&player, &x, &y, &value, &proof); @@ -192,7 +188,7 @@ fn test_win_condition_all_treasures_found() { let path = [(1u32, 0u32), (2u32, 0u32), (2u32, 1u32)]; for (x, y) in path { let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); } @@ -214,7 +210,7 @@ fn test_fog_of_war_sparse_root_updates() { let x = 1u32; let y = 0u32; let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); let after = client.get_state().fog_root; @@ -233,7 +229,7 @@ fn test_non_adjacent_move_rejected() { let x = 2u32; let y = 2u32; let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); } @@ -248,7 +244,7 @@ fn test_full_playable_sequence_to_win() { let sequence = [(1u32, 0u32), (2u32, 0u32), (2u32, 1u32)]; for (x, y) in sequence { let value = value_at(&map, x, y); - let proof = proof_vec(&env, &map, x, y); + let proof = proof_for(&env, &map, x, y); client.explore(&player, &x, &y, &value, &proof); } @@ -257,3 +253,55 @@ fn test_full_playable_sequence_to_win() { assert_eq!(state.player_state.health, DEFAULT_MAX_HEALTH - 1); assert_eq!(state.player_state.score, DEFAULT_TREASURE_VALUE * 2); } + +#[test] +fn test_commit_phase() { + // Tests that the game initializes in the committed state + let (env, client, player) = setup(); + env.mock_all_auths(); + let map = build_test_map(&env); + let root = map.tree.root_bytes(&env); + + client.init_game(&player, &root, &MAP_W, &MAP_H, &2u32); + + let state = client.get_state(); + assert_eq!(state.map_root.root, root); + assert_eq!(state.game_config.status, GameStatus::Active); +} + +#[test] +fn test_reveal_phase_with_valid_proof() { + // Tests the reveal/exploration phase with correct Merkle proof + let (env, client, player) = setup(); + env.mock_all_auths(); + let map = build_test_map(&env); + let root = map.tree.root_bytes(&env); + client.init_game(&player, &root, &MAP_W, &MAP_H, &99u32); + + let x = 1u32; + let y = 0u32; + let value = value_at(&map, x, y); + let proof = proof_for(&env, &map, x, y); + client.explore(&player, &x, &y, &value, &proof); + + let state = client.get_state(); + assert_eq!(state.player_state.x, x); + assert_eq!(state.player_state.y, y); +} + +#[test] +#[should_panic] +fn test_invalid_reveal_value_rejected() { + // Tests that providing an incorrect cell value during reveal is rejected + let (env, client, player) = setup(); + env.mock_all_auths(); + let map = build_test_map(&env); + let root = map.tree.root_bytes(&env); + client.init_game(&player, &root, &MAP_W, &MAP_H, &2u32); + + let x = 1u32; + let y = 0u32; + let proof = proof_for(&env, &map, x, y); + // Claim wrong value + client.explore(&player, &x, &y, &99, &proof); +}