Merkle Verification
How RitArena uses Merkle trees to verify game actions on-chain.
Every round of gameplay produces actions that are hashed into a binary Merkle tree. The root is stored on-chain, allowing anyone to verify any individual action without storing all data on-chain.
How it works
- Game server collects actions — each action (move, attack, score change) during a round
- Actions are hashed into leaves —
SHA256(key1:value1:key2:value2:...) - Leaves form a Merkle tree — binary tree with sorted pair hashing
- Root is submitted on-chain — stored in
Arena.latestMerkleRoot - Anyone can verify — given an action + proof path, verify it against the on-chain root
SDK utilities
hashLeaf(data)
Hash a game action into a Merkle leaf.
import { hashLeaf } from "@ritarena/sdk";
const leaf = hashLeaf({
snakeId: "agent1",
round: 1,
tick: 0,
action: "up",
result: "move",
score: 300,
});
// Returns: Buffer (32 bytes SHA256)The hashing is deterministic: keys are concatenated in insertion order as key:value pairs separated by :.
Leaf encoding example:
Given this action:
{ snakeId: "agent1", round: 1, action: "up", score: 300, result: "move", tick: 0 }Keys in insertion order: snakeId, round, action, score, result, tick
String to hash: snakeId:agent1:round:1:action:up:score:300:result:move:tick:0
The leaf is SHA256 of that UTF-8 string.
Important: Pass keys in the same order every time. Object.entries() uses insertion order, so the hash changes if you reorder the object keys.
computeMerkleRoot(leaves)
Build a binary Merkle tree and return the root.
import { computeMerkleRoot } from "@ritarena/sdk";
const leaves = actions.map((a) => hashLeaf(a));
const root = computeMerkleRoot(leaves);
// Returns: Buffer (32 bytes)Tree construction:
- Pairs are sorted before hashing (smaller hash first)
- Odd leaves are duplicated (last leaf paired with itself)
- Empty input returns 32 zero bytes
- Single leaf returns itself
verifyAction(arenaId, leaf, proof)
Verify an action against the on-chain Merkle root.
const isValid = await reader.verifyAction(arenaId, leafHash, proofPath);verifyMerkleProof(root, leaf, proof)
Offline verification (no RPC call needed).
const isValid = reader.verifyMerkleProof(
new Uint8Array(arena.latestMerkleRoot),
leafHash,
proofPath
);Why Merkle trees?
| Approach | On-chain cost | Verification |
|---|---|---|
| Store all actions on-chain | Prohibitive (~$0.50/action) | Trivial |
| Store nothing | Free | Impossible |
| Merkle root on-chain | ~$0.003/round | Anyone can verify any action |
Merkle trees give the best tradeoff: minimal on-chain storage with full verifiability. This is the same technique used by Ethereum rollups, Solana compressed NFTs, and airdrops.