Nethermind · zkVM tooling

Native C# for RISC-V64 zkVMs.

bflat-riscv64 compiles C# straight to fully static RISC-V64 ELF binaries — runnable inside a zkVM prover (Zisk today), under qemu-riscv64, or on real RISC-V64 Linux. One source, three targets.

RISC-V64 rv64ima · soft-float Zisk · zisk_sim musl-based Statically linked
C# IL .cs refs ELF .text .rodata PROGBITS RISC-V64 bflat ZK · READY

How it executes

What NativeAOT emits is what the prover proves.

A native RISC-V64 binary with full AOT optimization applied to your C# end-to-end. No IL interpreter, no JIT, no managed runtime in the loop — the prover walks the same instructions a C or Rust toolchain would have produced.

16modules

Link-time adaptations

2targets

zisk · zisk_sim

0syscalls

In Zisk builds

1command

Source → ELF

Run it now

No install. One command. Current directory mounted.

Drop a .cs file next to your shell and let the official container do the rest — driver, runtime, link-time modules, and postprocessor are all already inside the image.

your shell — bash
# Build for the Zisk zkVM — fully postprocessed, ready to prove
docker run --rm -v "$PWD:/work" -w /work \
    nethermindeth/bflat-riscv64:latest \
    bflat build hello.cs --os linux --libc zisk
# Build for the simulator — same source, runs under QEMU or native RISC-V64 Linux
docker run --rm -v "$PWD:/work" -w /work \
    nethermindeth/bflat-riscv64:latest \
    bflat build hello.cs --os linux --libc zisk_sim

The image is published on Docker Hub. Output ELF lands in the directory you mounted — already postprocessed and ready to feed straight into the Zisk prover.

The image is published on Docker Hub. Output ELF lands in the directory you mounted — runs under qemu-riscv64 or natively on RISC-V64 Linux, useful for debugging before proving.

What it is

A thin compiler driver around .NET NativeAOT, with the missing pieces a zkVM needs.

The original bflat compiles C# to native code via NativeAOT but produces dynamically linked binaries. None of those dependencies are acceptable inside a zkVM. Our fork adds two new libc targets, fifteen link-time modules, per-target linker scripts, and an ELF postprocessor — all without touching upstream sources.

Two new libc targets

zisk for proving inside Zisk; zisk_sim for a Zisk-shaped binary that still runs in QEMU or on real hardware.

Sixteen link-time modules

Small C / C++ / asm objects plug into the linker via --wrap= overrides. No upstream code is patched in place.

Custom linker scripts

Per-target memory layout: a fixed ROM/RAM split for Zisk and a single-segment layout for the simulator.

ELF postprocessor

A Python script that fixes .init_array and .tdata attributes for the Zisk loader and trims sections the prover doesn't load.

NuGet integration

--extlib understands manifest-tagged NuGet packages, including precompiled native libraries like libziskos.

Production-validated

Drives Nethermind's StatelessExecutor — a real Ethereum state-transition function — under continuous proof-time regression.

Foundation

A sibling project builds the base — bflat sits on top.

Before a single line of C# is compiled, the runtime and its cross rootfs are built so that every byte of the underlying environment respects the zkVM constraints — no compressed instructions, no FP, one ABI across the stack. They ship as release artifacts, downloaded and unpacked at bflat build time.

An official .NET build, not a fork.

The runtime is upstream .NET: the dotnet/dotnet VMR at a release branch, built with upstream's own build system. Everything a zkVM needs is applied around it — at ILC time, at link time, and after the link. What is left is a short list of riscv64 correctness fixes per .NET line, each written to be sent upstream, and after which this project would carry none.

3layers outside .NET

ILC substitutions · link-time modules · ELF postprocess

100%upstream sources

dotnet/dotnet at a release branch, built with upstream's own build system

0features carried

The minimal profile is riscv64 correctness fixes only — each one meant for upstream

2.NET lines

10 and 11, each with its own versioned profile

See every fixup and why it can't move out →

bflat pipeline

From .cs source to a zkVM-ready ELF in four stages.

On top of that foundation, bflat itself orchestrates four stages — all driven by BuildCommand.cs in src/bflat/. The C#-to-object compile uses Microsoft's stock NativeAOT; everything Nethermind contributes in this repo happens at link time and after.

NATIVE AOT Microsoft ILC .cs → .o stock, unpatched RISC-V64 object LD.LLD --wrap=* --whole-archive Static ELF PATCH ELF --fix-init-array --fix-tdata --remove-eh --trim-bss Loader-ready DEPLOY Zisk prover qemu-riscv64 Run anywhere 1 2 3 4 stock Microsoft ILC 12 link-time modules patch_elf.py zkVM · simulator
  1. 01

    Microsoft's NativeAOT (ILC) emits an object

    Stock, unmodified ILC compiles C# to a single RISC-V64 ELF relocatable containing managed code, type-system metadata, and module init tables. Using upstream Microsoft tooling keeps the C#-to-native step safe and universal — we inherit the same testing and portability the rest of .NET enjoys.

  2. 02

    ld.lld links the binary statically

    Module objects are pulled in with --whole-archive; --wrap=symbol redirects every musl, compiler-RT, and runtime call we need to override into our own implementations.

  3. 03

    patch_elf.py rewrites the ELF

    Fixes .init_array and .tdata attributes for the Zisk loader and trims .bss. Only runs for --libc zisk.

  4. 04

    Run anywhere — prove with Zisk

    The output is a single ELF. It runs natively on RISC-V64 Linux, under qemu-riscv64, and inside a zkVM prover (Zisk today). The same source builds for all three.

Architecture

A small surface that touches what it doesn't own.

Everything Nethermind contributes to this repo lives in one place: the compiler driver, the link-time modules, the linker scripts, and the postprocessor. The .NET runtime is built separately by dotnet-riscv, against an Alpine cross rootfs produced by upstream's own tooling (with musl rebuilt for the base ISA), and ships as release artifacts consumed at build time. No compressed and no FP instructions are emitted anywhere in the stack, and the code bflat compiles and links uses the soft-float lp64 ABI.

YOUR PROGRAM C# state-transition function · NethermindEth/nethermind extlib: libziskos extlib: ssz NATIVEAOT OUTPUT Compiled managed code · type system · module init tables · GC integration LINK-TIME MODULES · src/bflat/modules/ ubootstrap pal rhp rhp_native tls nofp rng_stupid security-stub stdcppshim rust_sys ugc-zero zkvm_zisk zkvm_zisk_sim Wired with --wrap=symbol overrides on a stock musl-based runtime. 16 modules · ~600 LoC at the largest · each one orthogonal · upstream sources unmodified. .NET RUNTIME · musl libc NethermindEth/dotnet-riscv · official VMR build · minimal fixups RISC-V64 · rv64ima · soft-float Zisk prover · zisk_sim · QEMU user-mode · native Linux
ubootstrap

Runtime entry point. Calls RhInitialize, registers the managed-code range, runs module initialisers, jumps to __managed__Main.

pal

Platform abstraction. getenv, sched_*, mmap, malloc/realloc/free, syscall — wrapped or routed to a downward bump allocator.

rhp

Object allocators on top of calloc, EventPipe stubs, default-locale, lock no-ops, custom RhpCidResolve, thread-static storage.

rhp_native

Hand-written RISC-V64 asm: write-without-write-barrier ref assignment and the RhpCidResolve trampoline.

tls

Minimal thread-local storage. A static 100 KiB buffer copies .tdata on first access and serves __tls_get_addr.

nofp

Empty bodies for soft-float compiler-RT helpers. Lets the binary link cleanly when the AOT pass eliminates floating point.

rng_stupid

Deterministic LCG-based PRNG that satisfies RandomNumberGenerator and OpenSSL requests.

security-stub

GSS / NetSecurity functions that all return -1. Prevents link errors for never-executed network paths.

gs_cookie

Stack canary pinned to a constant 0. No clock for entropy and no page protection in a zkVM, so the cookie is neutralised by design.

stdcppshim

C++ allocator shims. operator new[] and operator new forwarded to malloc.

rust_sys

Rust compatibility layer. sys_alloc_aligned wired to the bump allocator for adjacent precompiles.

ugc-zero

Drop-in GC that never collects. Acceptable because each proof is short-lived with a known working set.

zkvm_zisk · zkvm_zisk_sim

_start in asm + linker scripts. Sets gp, sp, tail-calls __libc_start_main.

Verification

Checks that fail early, not in the prover.

Six jobs run on every push and pull request in this repository: contracts on the native modules, C# analyzers on the driver, module unit tests on the real ISA under qemu, fuzzing, a CBMC proof of the allocator, and a build that produces the Docker images and links a guest for both zkVM targets. Running the samples and proving the Nethermind StatelessExecutor happen in a separate pipeline that reports to the zk-testing dashboard.

Commit git push to master CI build build-riscv64.yml Tests run samples + StatelessExec π Zisk proof deterministic, replayable Dashboard zk-testing.nethermind.dev CONTINUOUS VERIFICATION From contracts on a single function to a proof of the whole workload.

Repository CI

GitHub Actions build-riscv64.yml runs the contract gate, analyzers, unit tests, fuzzing and the CBMC proof, then rebuilds every module and the driver from source.

Sample regression

Samples under samples/ are built with zisk_sim and run under QEMU by the zk-testing pipeline, with output compared against a baseline.

End-to-end proofs

The zk-testing dashboard proves the Nethermind StatelessExecutor inside a zkVM and surfaces proof timings as a public badge.

See Verification for the full list of automated checks, the manual smoke tests, and how to read the dashboard.

Quick start

One command, source to ELF.

Assuming you have a built bflat in your $PATH:

~/work/hello — bash
# Compile a C# program for Zisk
$ bflat build hello.cs --os linux --libc zisk
$ ls -lh hello
-rwxr-xr-x  1 user  staff   2.5M hello

# Same source, simulator-friendly target
$ bflat build hello.cs --os linux --libc zisk_sim
$ qemu-riscv64 ./hello
Hello world!

# With an external Zisk-precompile library from NuGet
$ bflat build app.cs --os linux --libc zisk \
      --extlib NethermindEth/bflat-libziskos:1.0.0

Build something for a zkVM.

The runtime is downloaded automatically. The samples build in seconds. The pipeline is the same one Nethermind uses in production.