Nethermind · zkVM tooling

Native C# for RISC-V64 zkVMs.

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

RISC-V64 rv64ima · lp64d 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.

12modules

Link-time patches

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 — compiler, patched 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, twelve 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.

Twelve 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, removes EH tables, 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

Two sibling projects build the base — bflat sits on top.

Before bflat compiles a single line of C#, two upstream-derived projects ensure every byte of the underlying environment respects the zkVM constraints — no compressed instructions, no FP, one ABI across the stack. Both ship as release artifacts, downloaded and unpacked at bflat build time.

The .NET we ship is upstream — almost untouched.

A short, focused patch series sits between Microsoft's release branch and the runtime artifacts bflat consumes. Bumping to a new .NET version is a rebase, not a fork.

20patches

19 against the runtime VMR · 1 against the SDK

70files touched

Out of 56,796 tracked in dotnet/runtime
= 0.12 % · about 1 file in 811

+493net lines

Out of 11.1 M in upstream sources
= 0.004 % · +1 013 / −520

5categories

ISA · NativeAOT startup · zerolib · determinism · build infra

See every patch, grouped by purpose →

bflat pipeline

From .cs source to a Zisk-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

    Twelve 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, removes EH frames, 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 the Zisk prover. 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 and patched separately by dotnet-riscv; the underlying distribution is rebuilt from source by riscv-alpine-build with no compressed and no FP instructions emitted, while keeping the lp64d ABI for toolchain compatibility. Both ship as release artifacts consumed at build time.

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. 12 modules · 380 LoC at the largest · each one orthogonal · upstream sources unmodified. .NET RUNTIME · musl libc NethermindEth/dotnet-riscv · 20 patches · built out-of-tree RISC-V64 · rv64ima · lp64d Zisk prover · zisk_sim · QEMU user-mode · native Linux

Verification

Every commit walks the full pipeline.

Two CI jobs run on every push: build-riscv64 rebuilds the compiler and all modules from source; zk-testing runs the bundled samples and the Nethermind StatelessExecutor end-to-end through Zisk. If any link-time wrap, postprocessing pass, or runtime shim breaks, the dashboard turns red within minutes.

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 Every commit walks the full pipeline — code, build, run, prove.

Build CI

GitHub Actions build-riscv64.yml compiles every module and bflat itself. Artifacts are signed and reproducible.

Sample regression

Each sample under samples/ is built and run under zisk_sim.

End-to-end proofs

The zk-testing dashboard runs the full Nethermind StatelessExecutor through Zisk on every commit 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.