Building Web3 Games With Stellar & RISC Zero

Stellar x RISC Zero

The idea behind this tutorial was to create a on-chain game that demonstrated how to combine RISC Zero proofs with Stellar smart contracts.

James On YouTube

All code is open source on github: https://github.com/jamesbachini/typezero

The game is composed of four distinct components, all governed by deterministic rules to ensure that every participant computes the same result.

The frontend is responsible for capturing user keystrokes, encoding them into a compact replay format, computing preview statistics, and submitting proof results for verification. The user interface lives in frontend/app.mjs, while the deterministic replay encoding and scoring logic are implemented in frontend/src/replay.mjs
The backend exposes a minimal HTTP API that acts as a proving service. Its role is to normalize prompts, validate replay encodings, and invoke the RISC Zero prover to generate cryptographic proofs. The main entry point for this service is backend/server.js
The RISC Zero guest program is where verification and scoring are enforced. It deterministically replays the recorded events, applies timing constraints, computes the final statistics, and commits a fixed-size journal of public outputs. This logic is implemented in risc0/typing_proof/methods/guest/src/main.rs
The Stellar Soroban smart contract verifies the generated proof using a Groth16 verifier contract and maintains the on-chain leaderboard. It treats all off-chain components as untrusted and only updates state after successful verification. The contract implementation lives in contracts/leaderboard/src/lib.rs


Application Flow

  1. The frontend records keystrokes as (dt_ms, key) events and encodes them as bytes.
  2. The backend validates the replay and runs the RISC Zero host to generate a proof.
  3. The guest commits a small public journal containing:
    • challenge ID
    • prompt hash
    • player public key
    • score, WPM, accuracy, duration
  4. The frontend submits the journal outputs and proof seal to the Soroban contract.
  5. The contract verifies the proof and updates the leaderboard if the score is valid and superior.

Because the rules and arithmetic are deterministic, all parties compute identical results. The proof enforces that consistency.


Generating RISC Zero Proofs

The guest program validates timing constraints, replays input events, computes stats and commits a fixed-sized journal.

https://github.com/jamesbachini/typezero/blob/main/risc0/typing_proof/methods/guest/src/main.rs

Here is what the journal looks like:

let journal = encode_journal(
    challenge_id,
    &player_pubkey,
    &prompt_hash,
    score,
    wpm_x100,
    accuracy_bps,
    duration_ms as u32,
);

The host program executes the guest inside the zkVM and generates a proof that the guest code ran correctly and produced the committed journal outputs:

https://github.com/jamesbachini/typezero/blob/main/risc0/typing_proof/host/src/lib.rs

Note. that we need to output a groth16 receipt rather than a full receipt. Originally I was getting “Error 500: request body too large” from the RPC host. This was due to the seal size in the proof being too large for on-chain verification.

All anti-cheat guarantees live in this code and it is the trust layer for the entire application. If the replay violates constraints, no proof is produced. If this was going into production it would need further constraints and checks.


Verifying RISC Zero On Stellar

The contract treats both frontend and backend as untrusted. It:

Full code: https://github.com/jamesbachini/typezero/blob/main/contracts/leaderboard/src/lib.rs

Verification snippet:

fn verify_proof(
    env: &Env,
    verifier_id: &Address,
    journal_hash: &BytesN<32>,
    image_id: &BytesN<32>,
    seal: &Bytes,
) {
    let mut args: Vec<Val> = Vec::new(env);
    args.push_back(seal.into_val(env));
    args.push_back(image_id.into_val(env));
    args.push_back(journal_hash.into_val(env));

    let result =
        env.try_invoke_contract::<Val, InvokeError>(verifier_id, &Symbol::new(env, "verify"), args);
    match result {
        Ok(Ok(_)) => {}
        Ok(Err(_)) | Err(_) => panic_with_error!(env, Error::ProofVerificationFailed),
    }
}

Game Hub Integration

Stellar has an emerging web3 gaming ecosystem which follows a standardized format.

This contract will call the mock gamehub contract on testnet at:

CB4VZAT2U3UC6XFK3N23SKRF2NDCMP3QHJYMCHHFMZO7MRQO6DQ2EMYG

This includes the following interface:

#[contractclient(name = "GameHubClient")]
pub trait GameHub {
    fn start_game(
        env: Env,
        game_id: Address,
        session_id: u32,
        player1: Address,
        player2: Address,
        player1_points: i128,
        player2_points: i128,
    );

    fn end_game(
        env: Env,
        session_id: u32,
        player1_won: bool
    );
}

In this game player1 is the “house” and player2 is the user. We make a remote call to start_game() and end_game() in this external contract.

For more information on building games on Stellar visit:


Setup Instructions

The application relies on a backend and docker to generate the proofs. This isn’t in production so if you want to play with it you’ll have to clone it and set it up locally.

You’ll need Rust and the Stellar cli installed to get this up and running.

Start by cloning the repo, starting dockerd and running the make file.

git clone https://github.com/jamesbachini/typezero.git
cd typezero
sudo dockerd &
make dev

This deploys the contract to testnet, writes frontend config, and starts backend and frontend services.

Open up a browser on localhost port 5173 (don’t try port 3000 which is the backend)


TypeZERO demonstrates how devs can combine smart contract platforms with zero-knowledge technology.

Note the code is for demonstration purposes only.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.