Cachebox is a zk-app escape game built on the MINA blockchain. Hosted at zk-cachebox.herokuapp.com.
Running this app locally required node version 16+
local server
pnpm dev
build
pnpm build
production server
pnpm start
A value must be set for these environment variables for the smart contracts to work
VITE_GATE_KEY=12345
VITE_LAB_KEY=12345
VITE_UNLABELED_PW=example
VITE_SESSION_KEY=32_CHAR_VALUE # used to encrypt session cookie
The web app is built with svelte-kit. In general, the app is kept entirely client-side with SSR and pre-fetch disabled so that the behavior of snarkyjs, the smart contract language, is more predictable. There is one endpoint, gameState.ts
which handles a game state cookie. The other routes are an /about
page, and the game routes nested under /play
.
We use 3 stores to power the game. A store in svelte is similar to redux; it allows many components to share state. The first store is locationStore
, which is simply a list of game locations, and some behavior configurations. The other 2 stores are related to snarkyjs, the smart contract programming language. One boolean store, snarkyStore
, is set to true when the code has all been loaded and is ready to use. The other store deployedSnappStore
handles details about smart contracts that have already been deployed like their address and interface.
Each locaiton in the location store represents a route under the /play
directory. Any loction not explicitly handled by a route will fall back to the [tile-id]
route. Most of the navigation options in the pages are determined by the locationStore
.
The session cookie has this shape
interface SessionData {
user: string;
tile: string;
hasVisitedClearing: boolean;
hasVisitedLab: boolean;
gateProof: KeyProof;
labProof: KeyProof;
unlabeledRoomProof: KeyProof;
}
Tile is a location in the game, so that a user who leaves and comes back will return to where they left off. The hasVisited
values are used to unlock behavior. The proof
values are zero knowledge proofs that a user can use to assert completion of various puzzles in the game.
For snarky JS to work, these headers must be set.
'Cross-Origin-Embedder-Policy' => 'require-corp'
'Cross-Origin-Opener-Policy' => 'same-origin'
Svelte hooks should always set the headers correctly, but by changing config, especially SSR or prefetching, it is possible to bypass the hooks and miss the headers.
This project was built as part of the Mina ZK-App builders program. Mina is a zero knowledge focused blockchain which supports smart contracts written in javascript. At time of writing/building, there is not a public testnet, and some features are still missing from the sdk. For instance, the proofs that are generated by the game can't actually be verified. With that caveat, the following is how the Mina section of this app is organized.
There are a few smart contracts located in src/lib/snapps/not_used
which the game does not use. The Monty Hall one is the most interesting of those. The one smart contract that is being used is src/lib/snapps/escapeGameSnapp.ts
.
The smart contract stores the ciphertext of three encrypted values on chain. The ciphertext is split into two Field
s per value. A Field
is a zero-knowledge compatible data type for use in snarky js.
Note, this app does not use any typescript @decorators because they were not working well with snarkyjs on my production build. It is common to see decorators used to better illustrate what state and methods exist on a smart contract.
class EscapeGameSnapp extends SmartContract {
constructor(address: PublicKey) {
super(address);
this.gateKeyCT1 = State();
this.gateKeyCT2 = State();
this.labKeyCT1 = State();
this.labKeyCT2 = State();
this.unlabeledPwCT1 = State();
this.unlabeledPwCT2 = State();
}
...
}
interface EscapeGameSnappInterface {
address: PublicKey;
guessGateKey(key: string): Promise<KeyProof> | Promise<null>;
guessUnlabeledPw(key: string): Promise<KeyProof> | Promise<null>;
guessLabKey(key: string): Promise<KeyProof> | Promise<null>;
}
The proof system in src/lib/snarkyUtils/keyProof.ts
allows for proofs of knowledge to be made and combined. Since we have 3 encrypted values, there are 3 puzzles we can prove knowledge of. We can consider that a proof of any one puzzle might look like a boolean array of false
with one true
value at the index of the known puzzle. Or we can combine those arrays with a bitwise or to mean that multiple puzzles have been solved.
// First puzzle solved
[ 1, 0, 0]
// Second puzzle solved
[0, 1, 0]
// First and second puzzle solved
[1, 1, 0]