ARCHITECTURE

Battleships on Calimero

A multi-service P2P battleship game using namespaces, private storage, CRDT-based state sync, and a SHA256 commit-reveal anti-cheat scheme

2WASM Services
P2PNo Server
CRDTAuto-Sync
SHA256Commit-Reveal

How It Works

Two players on separate nodes, syncing state via CRDTs. Ship placements stay private until hit.

NODE 1 Lobby Service lobby.wasm Game Service game.wasm Shared State CRDT-synced Private Board NOT synced Calimero Runtime · merod NODE 2 Lobby Service lobby.wasm Game Service game.wasm Shared State CRDT-synced Private Board NOT synced Calimero Runtime · merod gossipsub
Shared state replicates via CRDT deltas over gossipsub. Both nodes converge automatically.
Private boards never leave the node. Shots are resolved locally, only hit/miss results are shared.

Namespaces & Access Control

A namespace scopes identity, governance, and app instances. Each battleships lobby is a namespace.

NAMESPACE root group · identity + governance + app binding Lobby Context service: lobby All members Player Pool Ed25519 identities Match Subgroup 2 players only Match Context service: game P1 + P2 only Default Capabilities (bitmask = 11) CREATE_CONTEXT INVITE_MEMBERS MANAGE_MEMBERS Bit 0 · create match contexts Bit 1 · invite to namespace Bit 3 · add to subgroups

Multi-Service Bundle

One .mpk bundle, two WASM services. Each context picks its service at creation time.

battleships-0.3.0.mpk multi-service application bundle Lobby Service lobby.wasm 6 methods create_match, set_match_context_id, get_matches, get_player_stats, get_history, on_match_finished 4 events · CRDT state Game Service game.wasm 12 methods place_ships (+commit), propose_shot, acknowledge_shot (+audit), reveal_board, export/import_board_seed, ... 9 events · flattened CRDT state manifest.json lobby-abi.json game-abi.json battleships-types GameError, PublicKey (no SDK dep)

Private Storage

Ship placements are the core secret: each player's board is stored locally and never replicated. A SHA256 commitment is published up-front so the placement cannot be changed mid-game.

Player 1's Node PRIVATE Ship positions + salt + pristine snapshot SHARED (CRDT) LwwRegister: turn, winner, pending, placed_p1/p2 UnorderedMap: shots_p1, shots_p2 (per-cell LWW) UserStorage: commitment_p1 (SHA256, writer-authed) Lobby: matches, stats (Counter), history (Vector) CRDT Player 2's Node SHARED (CRDT) LwwRegister: turn, winner, pending, placed_p1/p2 UnorderedMap: shots_p1, shots_p2 (per-cell LWW) UserStorage: commitment_p2 (SHA256, writer-authed) Lobby: matches, stats (Counter), history (Vector) PRIVATE Ship positions + salt + pristine snapshot
Commitment. At placement, each player computes SHA256(pristine_board || salt) and writes it to UserStorage. The slot is writer-authorised — only that player can ever write their own commitment, and a second write is rejected with AlreadyCommitted.
Reveal + audit. On the winning shot (or via reveal_board) the contract recomputes the hash against the pristine snapshot and replays every recorded shot against the revealed board. Tampering or lying hits produce AuditFailed.

Cross-Context Calls (xcall)

When a game ends, the game context notifies the lobby to update stats and history.

Game Context All ships sunk → winner determined calimero_sdk::env::xcall(...) JSON payload { match_id, winner, loser } Lobby Context on_match_finished() Update player_stats Append to history

Complete Game Flow

Eight steps from namespace creation to final score update.

1
Create Namespace
Host creates a namespace with the battleships app. Sets default capabilities (bits 0+1+3 = 11).
2
Create Lobby
Lobby context created in namespace root group with service_name: lobby
3
Invite Player
Recursive namespace invitation covers root + all subgroups.
4
Player Joins
joinNamespace → auto-gets identity → joins lobby context.
5
Create Match
Lobby allocates match ID. Create subgroup → add P2 → create game context with service_name: game
6
Place Ships + Commit
Ships stored in private storage. SHA256(pristine || salt) published to writer-authorised UserStorage. Write-once: second attempt returns AlreadyCommitted.
7
Take Turns
propose_shot → event triggers acknowledge_shot_handler on target node → resolves against private board → per-cell UnorderedMap entries merge via LWW.
8
Winning Shot + Audit
All ships sunk → inline audit recomputes commitment from pristine snapshot → replays every shot against the revealed board → AuditPassed/AuditFailedxcall to lobby updates Counter-backed stats.

CRDT State Sync

All shared state replicates automatically between nodes. Each mutation creates a delta broadcast via gossipsub. Primitives used: LwwRegister for single-writer fields, UnorderedMap for per-key add-wins, Counter for G-Counter stat increments, UserStorage for writer-authorised per-player slots, and Vector for append-only history.

DAG-based

Deltas form a causal DAG. Concurrent writes merge automatically without coordination.

Encrypted

Deltas are encrypted with the group's shared key before broadcast over the network.

Eventual

Nodes converge to the same root hash. No central coordinator needed.

The sync layer is service-aware: when applying a delta from a peer, the node loads the correct WASM service (lobby or game) based on the context's service_name to execute __calimero_sync_next.

Project Structure

battleships/
├─ app/ React + TS
│  ├─ src/hooks/ — useBattleshipsLobby, useNamespaceBootstrap
│  ├─ src/api/lobby/ — LobbyClient (codegen)
│  └─ src/api/game/ — GameClient (codegen)
├─ logic/ Cargo Workspace
│  ├─ crates/types/ — GameError (incl. audit variants), ExportedSeed, PublicKey
│  ├─ crates/lobby/ — LobbyState (CRDT collections) + 6 methods → lobby.wasm
│  ├─ crates/game/ — GameState (flattened CRDT fields) + 12 methods + audit.rs → game.wasm
│  └─ build-bundle.sh — builds both + packages .mpk
└─ e2e/ merobox — E2E workflow

Calimero Platform Features

Namespaces

Identity scoping, recursive invitations, subgroup-based access control

Multi-Service Bundles

Two WASM services in one .mpk, service_name-based context creation

Private Storage

#[app::private] for ship boards — never replicated between nodes

CRDT Sync

Automatic state replication with DAG-based causal ordering

xcall

Cross-context calls from game → lobby for match results

Event System

app::emit! for real-time UI updates via SSE subscriptions

Capabilities

Fine-grained permission bits for member actions

Governance DAG

Membership, roles, and context registration via signed ops

Subgroups

Per-match access isolation — only 2 players per match group

Commit-Reveal

SHA256 commitment in UserStorage + pristine-board reveal with full shot-replay audit