Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/risc0/risc0/llms.txt

Use this file to discover all available pages before exploring further.

Write zkVM guest programs in C to integrate legacy codebases or achieve low-level control over execution.

What You’ll Learn

  • Writing guest programs in C
  • Using the C platform API
  • Compiling C code for RISC-V zkVM
  • Reading inputs and committing outputs in C
This example is experimental and hasn’t been extensively tested. Use Rust for production applications when possible.

Overview

While most zkVM programs are written in Rust, you can also use C for:
  • Legacy Integration: Port existing C codebases
  • Low-Level Control: Direct memory management
  • Performance: Hand-optimized C code
  • Team Expertise: Leverage C programming skills

How It Works

1
C Guest Program
2
The guest multiplies two numbers in C:
3
#include "platform.h"
#include <assert.h>
#include <stdint.h>

union u32_cast {
    uint32_t value;
    uint8_t buffer[4];
};

int main() {
    // Initialize SHA-256 hasher for commits
    sha256_state* hasher = init_sha256();

    // Read two u32 values from host (little-endian)
    union u32_cast a;
    union u32_cast b;
    assert(env_read(a.buffer, 4) == 4);
    assert(env_read(b.buffer, 4) == 4);

    // Multiply
    a.value *= b.value;

    // Commit result to journal
    env_commit(hasher, a.buffer, sizeof(a.buffer));
    
    // Exit successfully
    env_exit(hasher, 0);

    return 0;
}
4
Platform API
5
The C platform API provides zkVM functionality:
6
#ifndef PLATFORM_H
#define PLATFORM_H

#include <stddef.h>
#include <stdint.h>

// SHA-256 state for journal commits
typedef struct sha256_state sha256_state;

// Initialize SHA-256 hasher
sha256_state* init_sha256(void);

// Read from host
size_t env_read(uint8_t* buf, size_t len);

// Commit to journal
void env_commit(sha256_state* hasher, const uint8_t* data, size_t len);

// Exit the guest program
void env_exit(sha256_state* hasher, int exit_code);

// Logging
void env_log(const char* msg);

#endif
7
Makefile
8
Build and run using Make:
9
# Build the C guest program
build:
	@echo "Building C guest program..."
	cargo build --release

# Execute in zkVM (fast, no proof)
execute: build
	@echo "Executing in zkVM..."
	cargo run --release --bin execute

# Generate proof (slow)
prove: build
	@echo "Generating proof..."
	cargo run --release --bin prove

# Run tests
test:
	cargo test --release

clean:
	cargo clean

.PHONY: build execute prove test clean

Running the Example

1
Install Dependencies
2
Install the RISC Zero toolchain:
3
curl -L https://risczero.com/install | bash
rzup install
4
Execute (Fast)
5
Run without generating a proof:
6
make execute
7
Prove (Slow)
8
Generate a full proof:
9
make prove
10
Test
11
Run the test suite:
12
make test

What Gets Proven?

The receipt proves:
  1. Correct Execution: The C program ran successfully
  2. Computation Result: The product is in the journal
  3. Input Processing: Two numbers were read and multiplied
  4. Deterministic Output: Result matches the committed value

C Platform API Reference

Reading Inputs

Read data from the host:
size_t env_read(uint8_t* buf, size_t len);
Parameters:
  • buf: Buffer to read into
  • len: Number of bytes to read
Returns: Number of bytes actually read Example:
uint32_t value;
assert(env_read((uint8_t*)&value, sizeof(value)) == sizeof(value));

Committing Outputs

Commit data to the journal:
void env_commit(sha256_state* hasher, const uint8_t* data, size_t len);
Parameters:
  • hasher: SHA-256 state from init_sha256()
  • data: Data to commit
  • len: Length of data
Example:
sha256_state* hasher = init_sha256();
uint32_t result = 42;
env_commit(hasher, (uint8_t*)&result, sizeof(result));

Exiting

Exit the guest program:
void env_exit(sha256_state* hasher, int exit_code);
Parameters:
  • hasher: SHA-256 state from init_sha256()
  • exit_code: Exit code (0 for success)
Example:
env_exit(hasher, 0);  // Success
env_exit(hasher, 1);  // Error

Logging

Log messages during execution:
void env_log(const char* msg);
Example:
env_log("Processing input...");
Logs are only visible during execution, not in the proof.

Memory Management

The zkVM provides a standard memory layout:
0x00000000  ────────  Code
            ...
0x00400000  ────────  Data
            ...
0x00800000  ────────  Heap
            ↑ grows up
            ...
0x80000000  ────────  Stack
            ↓ grows down

Dynamic Allocation

Use standard C allocation:
#include <stdlib.h>

// Allocate
int* array = malloc(100 * sizeof(int));
if (array == NULL) {
    env_log("Allocation failed");
    env_exit(hasher, 1);
}

// Use
for (int i = 0; i < 100; i++) {
    array[i] = i;
}

// Free
free(array);

Stack Usage

Be mindful of stack size:
// OK: Small stack allocation
int small_array[100];

// Risky: Large stack allocation
int large_array[1000000];  // May overflow stack

// Better: Use heap for large allocations
int* large_array = malloc(1000000 * sizeof(int));

Porting C Code

Compatible Features

Works in zkVM:
  • Standard library (stdio.h, stdlib.h, string.h, etc.)
  • Math functions (math.h)
  • Integer operations
  • Pointer arithmetic
  • Dynamic allocation
  • Function calls
  • Recursion
Doesn’t work:
  • System calls (open, socket, etc.)
  • Threading (pthread)
  • True randomness (rand() needs seeding)
  • File I/O (use env_read instead)
  • Time functions (time(), clock())

Porting Example

Original code:
#include <stdio.h>

int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("%d\n", a * b);
    return 0;
}
zkVM version:
#include "platform.h"

int main() {
    sha256_state* hasher = init_sha256();
    
    // Replace scanf with env_read
    int a, b;
    env_read((uint8_t*)&a, sizeof(a));
    env_read((uint8_t*)&b, sizeof(b));
    
    int result = a * b;
    
    // Replace printf with env_commit
    env_commit(hasher, (uint8_t*)&result, sizeof(result));
    env_exit(hasher, 0);
    
    return 0;
}

Use Cases

Legacy Cryptography

Port existing crypto implementations:
// Existing SHA-256 implementation
void sha256(const uint8_t* input, size_t len, uint8_t* output);

int main() {
    sha256_state* hasher = init_sha256();
    
    // Read input
    uint8_t input[1024];
    size_t len = env_read(input, sizeof(input));
    
    // Compute hash
    uint8_t hash[32];
    sha256(input, len, hash);
    
    // Commit result
    env_commit(hasher, hash, sizeof(hash));
    env_exit(hasher, 0);
    return 0;
}

Signal Processing

Port DSP algorithms:
#include <math.h>

void fft(float* real, float* imag, int n) {
    // FFT implementation
}

int main() {
    sha256_state* hasher = init_sha256();
    
    // Read signal data
    int n;
    env_read((uint8_t*)&n, sizeof(n));
    
    float* real = malloc(n * sizeof(float));
    float* imag = malloc(n * sizeof(float));
    
    env_read((uint8_t*)real, n * sizeof(float));
    
    // Process
    fft(real, imag, n);
    
    // Commit result
    env_commit(hasher, (uint8_t*)real, n * sizeof(float));
    
    free(real);
    free(imag);
    env_exit(hasher, 0);
    return 0;
}

Embedded Systems

Verify embedded algorithms:
// Controller logic from embedded system
float pid_controller(float setpoint, float measured) {
    static float integral = 0;
    static float previous_error = 0;
    
    float error = setpoint - measured;
    integral += error;
    float derivative = error - previous_error;
    previous_error = error;
    
    return KP * error + KI * integral + KD * derivative;
}

int main() {
    // Prove controller behaves correctly
    // Read inputs, run controller, commit outputs
}

Performance Considerations

Cycle Counts

C operations have predictable cycle costs:
OperationCycles (approx)
Addition1
Multiplication1
Division30-40
Function call2-3
Memory load1
Memory store1

Optimization Tips

Use integer math:
// Slower
float result = a * 3.14159f;

// Faster
int result = (a * 314159) / 100000;
Avoid division:
// Slower
int half = a / 2;

// Faster
int half = a >> 1;
Inline small functions:
static inline int max(int a, int b) {
    return a > b ? a : b;
}

Debugging

Printf Debugging

Use env_log for debugging:
char buf[100];
snprintf(buf, sizeof(buf), "Value: %d", value);
env_log(buf);

Assertions

Use assertions liberally:
#include <assert.h>

assert(value > 0);
assert(ptr != NULL);
assert(len <= MAX_SIZE);
Failed assertions will cause the guest to panic.

Comparison: C vs Rust

FeatureCRust
Memory safetyManualAutomatic
Learning curveModerateSteep
Legacy codeEasy to portNeeds rewrite
PerformanceExcellentExcellent
Safety guaranteesNoneStrong
Community supportMatureGrowing
Recommended forLegacy portsNew projects
Use Rust for new projects. Use C only when porting existing code or when team expertise requires it.

Limitations

Current limitations:
  • Experimental: Not as tested as Rust guests
  • No stdlib: Limited standard library support
  • Manual memory: No automatic safety
  • Build complexity: Requires custom toolchain
  • Debugging: Harder to debug than Rust

Next Steps