I built a two-player ZK word game on CKB. Come play it

It is Mastermind, except with words, and both people are playing at the same time. You each pick a secret four-letter word and commit to it on chain. Then you take turns guessing at each other’s word, and when someone guesses at yours you tell them how many hits they got (right letter, right place) and how many blows (right letter, wrong place).

The obvious hole in that is that you could just lie. So every answer comes with a Groth16 proof over BN254 saying the numbers really are what your committed word implies, and that your word is actually in the dictionary and not something you made up on the spot. Your word never leaves your browser. Only the proof does. The type script verifies it and runs the rest of the rules.

The verification rests on @Mulandi_Cecilia’s groth16-ckb, the on-chain Groth16 verifier for CKB-VM. That is the foundational work this is built on.

The bit I want people to look at

I wrote this same game on Starknet before, and that contract had a bug I did not see at the time. A proof does not say “this player answered honestly”. It says “some witness satisfies this circuit for these public inputs”. And the public inputs come in through the witness, which is written by whoever is spending the cell. So they can just make them up.

The fix is that the script has to bind every public input to state it read out of the game cell before it verifies anything: the proven guess against the guess actually pending, the proven commitment against that player’s committed hash, the proven hits and blows against what the output cell is about to claim. Miss any one of those and the proof is decoration. My Starknet version missed three.

If anyone has time to poke holes in that part, I would really appreciate it.
Everything else is polish, that is the thing the game stands on.

Numbers

Circuit 3,149 constraints, four public inputs, about 360 ms to prove in the browser
Dictionary 1,807 words, membership proven with a Poseidon Merkle path
Script 93,232 bytes
One answering move 98,209,564 cycles in ckb-testtool, 105,988,770 on testnet once the lock is included
Each public input around 285,000 cycles, which is why I stopped at four

Precomputing the verifying key into its own cell drops that move to 70,463,905 cycles, 28.3% cheaper, for 384 more bytes on chain. It works and I measured it, but it is off by default because it means shipping another artifact.

Where it is

Testnet, and testnet only. The verifier under it is pre-audit and says so, so please do not put this on mainnet.

If your opponent walks off you can claim the game after a timeout.

Code is at GitHub - truthixify/ckb-mastermind: A two-player zero-knowledge word Mastermind duel on Nervos CKB. Hit/blow feedback proven honest with a Groth16 proof over BN254, verified inside a CKB type script that also enforces the game's state machine. Testnet only. · GitHub.

Happy to answer anything about the circuit or the cell state machine.

And thanks again to @Mulandi_Cecilia for groth16-ckb. None of this happens without it.

14 Likes

this is much like a commit–reveal game, and it neutralizes first-mover advantage by deciding the outcome only after both players receive an equal number of attempts.

however,

    1. create/join should also prove that each commitment represents a valid dictionary word, otherwise a player retains a free option to enter first and abandon later.
    1. inaction is now costly with timeout forfeiture, but zero-stake games can still become a war of attrition in which the staller externalizes hours of waiting onto the opponent.
flowchart LR
    A["Private-information game<br/>Secret words and unverifiable feedback"] --> B{"Player's strategic options"}

    B --> C["Answer honestly"]
    B --> D["Misreport feedback"]
    B --> E["Delay or abandon"]
    B --> F["Commit to an invalid word"]

    D --> G["ZK proof plus state binding"]
    G --> H["Verifiable signaling<br/>eliminates cheap talk"]

    H --> I["Exchange-boundary settlement"]
    I --> J["Equal attempts for both players"]
    J --> K["Comparable to sealed-bid<br/>and commit–reveal games"]

    E --> L["Timeout and forfeiture"]
    L --> M{"Is value at stake?"}
    M -->|"Yes"| N["Default becomes costly<br/>credible commitment"]
    M -->|"No"| O["Low-cost griefing<br/>war of attrition"]

    F --> P["Commitment is accepted<br/>before validity is proven"]
    P --> Q["Player receives a free<br/>abandonment option"]

    Q --> R["Recommendation<br/>Prove commitment validity<br/>during create or join"]
    O --> S["Recommendation<br/>Refundable activity bond<br/>paid to opponent on default"]

    classDef strength fill:#DDF6E8,stroke:#23855B,color:#12372A,stroke-width:2px;
    classDef risk fill:#FFF0D6,stroke:#D18A16,color:#543400,stroke-width:2px;
    classDef remedy fill:#E7EDFF,stroke:#496FD8,color:#15285D,stroke-width:2px;

    class G,H,I,J,K,N strength;
    class E,F,O,P,Q risk;
    class R,S remedy;
4 Likes

These issues are real, thank you @ArthurZhang

1 Like

Although the mastermind circuit binds the commitment to the word and proves dictionary membership, so a junk commitment means you can never answer at all. A player will enter a game they can’t finish.

1 Like