PKMN_BTTL: A Pokemon Battle Game, Written in Zig and Executed with BitVMX

Rootstock Labs,Fede Jinch
·
January 22, 2025
·

This article was originally published on Rootstock Labs’s webpage and is included on BitVMX’s site. Fairgate and Rootstock Labs are the two founding sponsors of the BitVMX project.

Rootstock Labs (Fede Jinch)

BitVMX is a two-party optimistic execution protocol that allows the execution of arbitrary computations on Bitcoin. This enables transactions where the spending condition depends on the result of such computation. In this article, we will use it to create a Pokemon-like game, where players must select the right character to battle and claim the locked Bitcoins (BTC).

In this article, we will:

  • See how we built the Pokemon “game”
  • Learn to compile Zig into a RISC-V32IM program
  • Learn about BitVMX program restrictions
  • Learn how to use the BitVMX protocol to execute a program on Bitcoin

In this article we won’t:

  • Dive too deep into how BitVMX works, you can read the whitepaper 🙂

This article will be based on the following repo, which already contains all the files to run the game.

About BitVMX

BitVMX is the result of a collaboration between RootstockLabs and Fairgate Labs. This joint research has already demonstrated the first-of-its-kind SNARK proof verification on Bitcoin mainnet, and the potential for further advancements, such as trust-minimized Bitcoin bridges, decentralized exchanges (DEXes), oracles, etc.

BitVMX has also been the core element of building the new Union Bridge ; the first complete design of a secure and efficient bridge that utilizes BitVMX to transfer BTC between networks while preserving Bitcoin’s security guarantees.

The Game

First, why? As we just mentioned, we have already used the protocol for two big infrastructure use cases, which are really interesting and challenging but they are not so funny. So, for this time, we decided to build a funny use case and show how easy/simple it is to use the BitVMX verifier on Bitcoin…. we realized that we could do something with Pokemon and I always loved Pokemons. We decided to go in that direction and now we can claim that we are the first ones to implement a Pokemon game on Bitcoin. :)

We will build PKMN_BTTL, a simple “game” where two parties lock some bitcoins in a BitVMX contract, and one player needs to provide an input to select a Pokemon to battle against Charizard. If the player’s Pokemon wins the battle, the player can build a proof and verify it using BitVMX to claim the locked Bitcoins.

The Pokemon battle is fully simulated, but since in this version we are not randomizing the battle outcome, the result will be deterministic and depend on the Pokemon chosen by the player.

The user can pick one out of these three options:

  • Input 0x0000_1234 : selects Pikachu, which wins the battle.
  • Input 0x0000_1235 : selects Snorlax, which loses the battle.
  • Input 0x0000_1236 : selects Magmar, which loses the battle.
  • Any other input value results in an error.

To simulate the Pokemon fight we use a battle engine written in Zig.


# pkmn_bttl/pkmn_bttl.zig

const pkmn = @import(“pkmn”); 
const INPUT_ADDRESS: usize = 0xAA00_0000;

pub export fn main() u32 {
    // selects pokemon by input
    var selected_pokemon = pokemon_by(@ptrFromInt(INPUT_ADDRESS).*);

    // battle against charizard using pkmn/engine
    var battle = new_battle(selected_pokemon, .Charizard); 
    var result = battle.update(CHOICE_1, CHOICE_2, &OPTIONS) catch return ExecutionResult.BATTLE_ERROR;
    while (result.type == .None)
        result = battle.update(CHOICE_1, CHOICE_2, &OPTIONS) catch return ExecutionResult.BATTLE_ERROR;

    // Execution.WIN makes the program end with zero, otherwise it panics
    return switch (result.type) {
        .Win => 0, 
        else => @panic(“Battle lost”),
    };
}   

The game reads the player’s input from a fixed memory address, selects the corresponding Pokemon, and starts a battle against Charizard. The battle runs in a loop, updating until a result is reached. If the player wins, the function returns success; otherwise, it stops the program.

Compile from Zig into RISC-V

To compile the game for a RISC-V architecture, we first define an entry point that points to our main() function and we specify the memory layout using a link script:


ENTRY(_start)

SECTIONS
{
    .text 0x80000000: ALIGN(4) {
        *(.text)
        . = ALIGN(4);
    }
    .data 0x90000000 : ALIGN(4) {
        *(.data)
        . = ALIGN(4);
    }
    .bss 0xA0000000 :  ALIGN(4) {
        *(.bss)
        . = ALIGN(4);
    } 
    .stack 0xE0000000 :  ALIGN(4) {
        *(.stack)
        . = ALIGN(4);
    }
    .input 0xAA000000 :  ALIGN(4) {
        *(.input)
        . = ALIGN(4);
    }
    /* CPU-registers “hardcoded” section */
    .registers 0xF0000000 : ALIGN(4) {
        *(.registers)
        . = ALIGN(4);
    }
    .rodata 0xB0000000 : ALIGN(4) {
        *(.rodata) 
        . = ALIGN(4);
    }
}    

We define memory regions for different components of execution. Sections and program code are aligned to 4-bytes. We allocate the .text section for executable code at 0x80000000, followed by .data and .bss at 0x90000000 and 0xA0000000 for initialized and uninitialized data, respectively. We reserve a dedicated stack region at 0xE0000000 and an .input section at 0xAA000000 to handle the user input. Constants and read-only data are placed in .rodata at 0xB0000000, while a special .registers section at 0xF0000000 maps CPU registers to a fixed address.

We use a build.zig file to configure the build process of the program, targeting the riscv32 architecture with the I and M extensions enabled, where the I extension supports basic integer operations, and the M extension adds multiplication and division capabilities. We also declare the previously mentioned entry point (entrypoint.s) and linker script (link.ld).


# pkmn_bttl/build.zig

...

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});

    // enable I and M extensions
    const features = Target.riscv.Feature;

    var enabled_features = Feature.Set.empty;
    enabled_features.addFeature(@intFromEnum(features.i));
    enabled_features.addFeature(@intFromEnum(features.m));

    // setup riscv32 im target
    const riscv_target = CrossTarget{
        .cpu_arch = .riscv32,
        .cpu_model = .{ 
            .explicit = &std.Target.riscv.cpu.generic_rv32
        },
        .abi = .gnu,
        .os_tag = .freestanding, 
        // this doesn't run on any operating system zig is aware of
        .cpu_features_add = enabled_features,
    };
    const resolved_target = b.resolveTargetQuery(riscv_target);
    ...

// specify entrypoint and custom linker script
exe.addAssemblyFile(b.path("entrypoint.s"));
exe.setLinkerScriptPath(b.path("link.ld"));
...

Now we compile the program:


zig build
# compiles it using build.zig (at PROJ_ROOT/pkmn_guess)

This generates the RISC-V .elf at pkmn_guess/zig-out/bin/pkmn_guess.

Running on the CPU Emulator

After compiling we execute it in the RISC-V CPU emulator, providing the right input, to validate the outcome before running it on Bitcoin.


# runs pkmn_guess.elf (at PROJ_ROOT/bitvmx_protocol/BitVMX-CPU)
cargo run --release --bin emulator -- execute --elf pkmn_guess.elf --debug --checkpoints --input 00001234 --input-as-little

And we get the output:

...
Exit code: 0x00000000
Register 0: 0x00000000
Register 1: 0x8001f6dc
Register 2: 0xe0800000
Register 3: 0x00000000
Register 4: 0x00000000
Register 5: 0x80000000
Register 6: 0x80030128
Register 7: 0xaaaaaaaa
Register 8: 0x00000000
Register 9: 0x00000000
Register 10: 0x00000000
Register 11: 0x00000000
Register 12: 0x00000000
Register 13: 0x00000000
Register 14: 0xe07ffcb8
Register 15: 0x00000001
Register 16: 0xaaaaaaaa
Register 17: 0x0000005d
Register 18: 0x00000000
Register 19: 0x00000000
Register 20: 0x00000000
Register 21: 0x00000000
Register 22: 0x00000000
Register 23: 0x00000000
Register 24: 0x00000000
Register 25: 0x00000000
Register 26: 0x00000000
Register 27: 0x00000000
Register 28: 0xaaaaaaaa
Register 29: 0x00000000
Register 30: 0xaaaaaaaa
Register 31: 0x00000000
Total steps: 51987 0x000000000000cb13
Last hash: 25098eb5864840d2a27223244cc920763bd01a5d7c95b6eb534cb5e7ea547fac

Running the program on the emulator is really fast (it takes about a second) and if we run it with the right input it results in an exit code zero, meaning that we won the battle.

We are ready to set up and execute the program on Bitcoin.

BitVMX Supported Programs

In BitVMX, we can run “any” type of computation. However, this depends on the limitations of the RISC-V CPU emulator, which currently has the following restrictions:

Supports the RISC-V 32 instruction set, specifically the riscv32im architecture that includes the (I) integer and (M) multiplication/division extensions. Programs must be compiled and linked into a valid ELF executable

Only supports the following syscalls:
exit (93)
syslog (116)
Input data must be manually placed into a designated memory section.
In this article, we provide the input in the “.input” section.
Programs must have properly structured sections like .text (code), .data (initialized data), and .bss (uninitialized data) according to the ELF conventions. The program must define an entry point to the main function.
The sections and program code must be aligned to 32 bits (4-byte) boundaries. The program must respect the ABI for RISC-V, including conventions for register usage (x0 is always zero, sp for the stack pointer, ra for the return address, etc.).
There is a dedicated “hardcoded” memory section for the CPU registers (starting at 0xF0000000) that must be preserved untouched.
Additionally, programs are required to comply with the protocol’s restrictions:

They should be deterministic to allow verification by other participants.
max_amount_of_steps parameter needs to be defined as the maximum number of steps for a program for any case.
It is safe to add extra steps as a safeguard, otherwise, any valid program that exceeds the maximum number of steps would be easily challenged as invalid.

Running on Bitcoin

Now it’s time to run the Pokemon battle and verify its execution on Bitcoin to claim the locked funds. In this article, we will do it on a bitcoin-request node, and the prover will provide the right input but miscompute step number 10. An honest verifier detects this wrong behavior and starts a challenge process. We will see how we do this in the following steps:

1. Place the compiled pkmn_bttl.elf into the required directories

  1. bitvmx_protocol/BitVMX-CPU/docker-riscv32/riscv32/build/
  2. bitvmx_protocol/execution_files/

2. Generate Commitment File: Use the BitVMX-CPU emulator to generate the ROM commitment for the pkmn_bttl.elf :

cargo run --release --bin emulator -- generate-rom-commitment --elf pkmn_bttl.elf --sections > ../execution_files/pkmn_rom_commitment.txt

The ROM commitment is a cryptographic proof that ensures the integrity and authenticity of the pkmn_bttl.elf program. This ties the protocol to a specific version of the agreed program, allowing both parties to verify that the program they are running is the same .elf as the one originally committed to.

3. Generate the Instruction Mapping: Use the BitVMX-CPU emulator to generate the instruction mapping, which generates the Bitcoin script mapping for every RISCV opcode:

cargo run --release --bin emulator -- instruction-mapping > ../execution_files/instruction_mapping.txt

4. Prepare bitvmx_protocol: Add the files to run the program on Bitcoin


# bitvmx_protocol/../execution_trace_generation_service.py

class ExecutionTraceGenerationService:
    @staticmethod
    def elf_file():
    return "pkmn_bttl.elf"

    @staticmethod
    def commitment_file():
    return "./execution_files/pkmn_rom_commitment.txt"

As previously mentioned, we intentionally miscompute step 10 to see how a challenge process works. We specify this in the BitVMXWrapper:

# bitvmx_protocol/../bitvmx_wrapper.py

class BitVMXWrapper:
    def __init__(self, base_path: str):
        self.base_path = base_path
        self.execution_checkpoint_interval = 50000000
        self.fail_actor = "prover"
        self.fail_step = "10"
        self.fail_type = "--fail-execute" # adds one to trace_write_value

5. Start Services: At this point, we have to launch each actor. Start a bitcoin regtest node and then start prover and verifier services. Do this by building and running three different docker services:

docker compose build # builds the docker setup
docker compose up bitcoin-regtest-node # runs docker bitcoin-regtest service
docker compose up prover-backend # runs docker prover service
docker compose up verifier-backend # runs docker verifier service

The prover will provide a SwaggerUI at http://0.0.0.0:8081/docs#/v1 , to interact with the protocol in a more friendly way.

6. Setup (and fund): The prover starts the setup by calling the /api/v1/setup/fund. This endpoint also locks funds for the game into the agreed address. Configure the protocol as follows:

{
    "max_amount_of_steps": 51987,
    "amount_of_bits_wrong_step_search": 3,
    "secret_origin_of_funds": "7920e3e47f7c977dab446d6d55ee679241b13c28edf363d519866ede017ef1b4",
    "amount_of_input_words": 1 // only one word == 00001234
}

7. Submit Input: The prover uses the /api/v1/input endpoint to submit the input required for the program execution (the pokemon choice).

{
    "input_hex": "00001234", // invokes pikachu -> winning option
    "setup_uuid": "0fa16fb9-236d-4653-9bbd-f64bbc70292c"
}

8. Program Verification: At this stage, the prover tries to prove that the battle program executes correctly, but the verifier detects a different final state and triggers the dispute resolution protocol. This initiates an n-ary search mechanism to find the first wrong step. The process consists of several rounds, where the prover presents small chunks of the intermediate states to the verifier until the counterparty finds that step number 10 produces a different trace.

Once identified, the verifier requests a more detailed trace (full trace) to identify the precise nature of the failure. In this case, the verifier detects that the write value is different and broadcasts the special execution challenge transaction, which after a timelock allows him to claim the locked funds, finalizing the dispute and making the prover lose the Pokemon game. 🙁

Summary

That’s it! In this article, we have successfully built a Pokemon “game” in Zig, compiled into RISC-V, and run it on Bitcoin using BitVMX. We set up the protocol and tried to spend Bitcoins that were tied to the battle result. Since one of both actors (the prover) miscomputed, the dispute resolution protocol was triggered and finalized with the honest actor (the verifier) detecting the wrong step, identifying the exact error cause, and claiming the locked funds. Although it didn’t end as the prover expected, we can say that any type of computation can be executed on Bitcoin.… even Pokemon battles!!!

Resources

Join our community