Client Side Proofs

Write up by 5p0rt5BEArD, vision and code by Farms

Verifiable off-chain game execution and data.

We have developed a way of practically using ZK Proof technology to execute our game logic in client code that is verifiable on-chain. SNARKs are combined with off-chain data to verifiably update game state with minimal gas costs. It’s practical to use in our game development right now, whilst also paving the way to take advantage of future technology and free-up the game design space even further.

The experimental code used to demonstrate the method is available at github.com/playmint/exp-snark-combat. You can jump straight in and follow the readme there to start playing with the code. The rest of this article explains why we are using this tech, its advantages, its limitations and where it might go in future.

Fully on-chain games

At Playmint we believe in the future of decentralised games that enable player ownership of content and community ownership of games. Blockchain can be used to deliver this but it’s important that the game world, its state and the logic which updates it, can be verified by the chain.

There’s lots to dig into on this subject and I’ve put a few good references below if you want to explore the motivations for being fully on-chain. For us, being fully on-chain is critical and everything we build is with that in mind.

Constrained design

We are first and foremost a games studio and current blockchain technology constrains the kind of games we can make. We still need to deliver rich and engaging game experiences so working within the constraints is part of the job.

As part of the Loot ecosystem we released The Crypt, a fully on-chain game where players collaborated to defeat dungeons with their Loot bags. The Loot NFT is on Ethereum so we deployed our first dungeon logic contracts there. With gas costs measured in tens of dollars, we constrained our game design to one transaction per play: RaidDungeon(lootID).

Raid a Dungeon with a Loot NFT - all executed on Ethereum.
Raid a Dungeon with a Loot NFT - all executed on Ethereum.

That one transaction triggered interesting score mechanics that incorporated Loot lore, provided motivations for collaboration and generated competition for the best prizes. So, The Crypt showed it’s possible to make fun experiences that form deep play patterns with a small number of transactions. However, we want to deliver a richer core game experience that engages players for longer.

For our next game we want to have more moves and more complex moves. We’re now making an MMO where we’re expecting the average player to make 30 moves in a day and for each action to generate meaningful interaction with other players. This would be prohibitively expensive on Ethereum but we believe that Ethereum is the best place to secure our games so we are hitting the scaling problem.

Ethereum Scaling

Ethereum’s high transaction costs, which rise with network congestion, is obviously not a unique problem to Playmint. There is a huge amount of work going into both improving the network itself and into many different Layer 2 scaling solutions.

L2 Rollups are achieving scale by batching transactions and compressing the amount of data which is settled on L1. ZK Rollups in particular allow logic to be executed off-chain and many L2 designs promise to move state storage off Ethereum. However, they are all still maturing and won’t realise their full potential until planned upgrades to L1 Ethereum. Part of the solution, for now, is to use alternative chains that deliver scaling without making too many compromises on decentralisation but we also need to develop our own on-chain game framework that is optimised to work on those chains.

Off-Chain Execution and Storage

A key property of ZK Proofs is that they allow execution correctness to be verified independently from where the execution occurs. We are writing game-specific proofs that can be generated in client code and verified on chain. This allows our game to grow in complexity while on-chain execution time remains flat.

However, logic calculations alone do not account for transaction costs and contract state storage is one of the biggest gas guzzlers. Our proofs need to be verified on-chain, which means we need to have the relevant data in state storage. Tests (which can be seen in the gas-compare branch of our repo) showed we lose all the benefits of off chain execution if all the data required to verify them is written to, and read back from contract storage.

Client side execution and off-chain storage.
Client side execution and off-chain storage.

We’re solving this with an action-claim pattern that requires only a hash of the player actions to be written to contract storage while action parameters remain in transaction data. Client-side, the full data is available to generate proofs. On-chain, only the hash needs to be in storage to verify proofs.

SNARKs as Sessions with Ticks

We are using SNARKs as opposed to STARKs, which are currently impractical to generate on client machines during gameplay due to proof times, proof sizes and the immaturity of general purpose tools. SNARKs have their own limitations including being non-interactive which means they can only work on a predefined limited set of inputs.

We have developed a game design pattern of sessions to fit in with the SNARK constraints.

Sessions:

  • Multiplayer with a maximum number of participants.

  • Time limited, lasting a fixed amount of time, measured in hours.

  • Activated when the first player joins.

  • Split into logical update ‘ticks’, each being a number of blocks.

  • Game logic, loops for each tick, evaluates all players, updates their attributes and any session attributes.

  • This game logic is written as an arithmetic circuit, which is the provable bit that can be executed by the client code (browser, Unity or whatever).

  • At any time the circuit can be executed to know the current attributes.

  • At any time the output from the circuit can be used to make on-chain claims.

A PvE Combat example:

  • Enemy has X health (Session attributes).

  • Player 1 discovers the enemy and starts attacking (Session Starts).

  • Other plays can then join the fight.

  • Each player delivers Y damage per ‘tick’.

  • If the Enemy reaches 0 health within the time limit, players can claim the reward of rune NFT.

We follow this pattern for various parts of the game, which allows us to have more players interacting with the world at once for minimal gas cost. Our new constraint for these sessions becomes proving time, which we aim to keep under 30 seconds for most client machines.

Gas Costs remain fixed even as complexity rises.
Gas Costs remain fixed even as complexity rises.

Where we use this pattern in our game, the gas costs to claim rewards remains flat regardless of the complexity, i.e. it’s the same gas cost, regardless of how many ticks the session runs for and how many players take part.

The Future

The target is all game state being calculated and stored off-chain, leaving the blockchain to do what it does best: provide decentralised consensus on transactions. Only when making claims based on game state, like transferring ownership of an earned character, will a proof of state be needed and verified on chain to make claims.

This target matches the roadmap of L2s like StarkNet which are likely to be part of the future tech stack but game specific solutions will need to be built on top. The game specific layer will make use of developing ZK Proof technologies like recursive proofs and general purpose STARK VMs that generate practically sized proofs. We’re closely watching projects like Polygon Miden and RiscZero as examples of technology that could help build the game specific part of the on-chain stack.

While important tools and infrastructure are being built on Ethereum, we’re targeting the Polygon PoS chain due to its current good balance of decentralisation, security and scalability. Using our SNARK session system gives us practical gas savings right now as well as helping us build a framework and train our game development muscle memory for writing optimal on-chain games.

Further Reading

We are not presenting new core technology here but we have found a way of using production ready technology to expand the current possibilities in on-chain gaming. We wanted to share what we’ve built to open opportunities for collaboration and feedback. Our actual game code will also be open source before public release as part of our decentralised roadmap.

There are many, many other builders and researchers advancing this space and here’s a list of a few we’re taking inspiration from:

Decentralised Game Theses

ZK Proof Battling

  • Excellent walk through for writing a battler with Cairo proofs by Killari.

  • Hallucinations and Battle Rollups, by Fraser Scott. (He has developed a system very close to our client side SNARK sessions, which we only discovered after meeting him - there’s lots of people tacking this problem in lots of ways)

On-Chain Game Frameworks

  • MUD, a truly composable on-chain game framework by Lattice.

  • RYO, a modular contract framework for StarkNet by dopeDAO.

ZK Proof Theory

There’s lots of information out there and you can go quite deep but a good start is an accessible comparison of SNARks vs STARKs by Bobbin Threadbare.

Implementation

A walk through the key parts of our experimental code.

Build time

The proofs are written as circuits in Circom. (see combat.circom)

template Combat(numSeekers, numTicks) { 
  signal input dungeonAttackArmour[numTicks][numSeekers];
  signal input ...

snarkJS is used to compile the circuits to wasm and to generate a solidity verifier contract. (see the Makefile)

verifier.sol: combat_0001.zkey
  npx snarkjs zkey export solidityverifier combat_0001.zkey

combat_js/combat.wasm: circuits/combat.circom
  rm -rf combat_js
  circom $< --wasm --sym

Run Time

NB. In the code we’ve shared, the client is typescript running directly in the browser. The game we’re building has a Unity client and a GraphQL interface to the blockchain, so these functions will be c# code making GraphQL calls.

Client code takes player inputs and submits them as on-chain transactions. (see index.tsx::ActionButton)

const send = async () => {
  return dungeonContract.send(kind, seekerID, ...);
};

Solidity code stores a hash of the action data and emits an event. (see Dunegon.sol::commitAction)

function commitAction(
  ActionKind actionKind, uint8 slotID, uint8[7] memory args)
  public {
  
  // update the hash
  slots[slotID].hash = hasher.poseidon(slots[slotID].hash, ... );
        
  // log the action data
  emit Action(actionKind, slotID, args);
}

Client code reads contract events and uses snarkJS to generate the current game state (which also contains the proof). (see index.tsx::getGameState)

const inputs = await
  generateInputs(slots, currentSeeker, currentTick);

const outputs = await
  snarkjs.groth16.fullProve(inputs, 'combat.wasm', 'combat.zkey');

The client can just display game state at this stage.

If the player wants to make a claim, the gameState (which contains the proof) is sent as a parameter to an on-chain transaction. (see index.tsx::handleClaimRune)

const handleClaimRune = () => {
  dungeonContract.claimRune(gameState as any)
};

Solidity code combines the passed in state with the action hashes saved to contract storage and passes it all on to the verifier contract. (see Dunegon.sol::verifyState)

function verifyState(CombatState memory state) 
  public view returns (bool)
{
  uint[NUM_SEEKERS+6] memory input;
  uint i = 0;
  input[i] = state.dungeonArmour;
...
  for (uint s=0; s<NUM_SEEKERS; s++) {
    input[i] = slots[s].hash;
    i++;
  }
...
  return combatVerifierContract.verifyProof(
    state.pi_a, state.pi_b, state.pi_c, input);
}

If verification passes, the solidity code rewards the player, which is a change in on-chain state storage. (see dungeon.sol::claimRune)

function claimRune(CombatState memory state) public {
  ...
  runeContract.mint(tx.origin, dungeonRewardRuneAlignment);
}

And so the combat rewards are safely granted on-chain without any battle logic running on-chain.

Subscribe to 5p0rt5BEArD
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.