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.
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.
Link-time patches
zisk · zisk_sim
In Zisk builds
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.
# 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
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.
Alpine — riscv-alpine-build
The whole Alpine distribution rebuilt from source. GCC
reconfigured to drop compressed and floating-point instruction
emission while keeping the lp64d ABI. ~320 lines of
patches do the entire system-wide rebuild.
.NET — dotnet-riscv
Pulls a tagged upstream .NET VMR, applies 20 focused patches (RISC-V64 ISA constraints, NativeAOT startup, determinism for proofs) and packages CoreLib, runtime libraries, GC, and bflat-side objects as a downloadable release.
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.
19 against the runtime VMR · 1 against the SDK
Out of 56,796 tracked in dotnet/runtime
= 0.12 % · about 1 file in 811
Out of 11.1 M in upstream sources
= 0.004 % · +1 013 / −520
ISA · NativeAOT startup · zerolib · determinism · build infra
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.
-
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.
-
02
ld.lld links the binary statically
Twelve module objects are pulled in with
--whole-archive;--wrap=symbolredirects every musl, compiler-RT, and runtime call we need to override into our own implementations. -
03
patch_elf.py rewrites the ELF
Fixes
.init_arrayand.tdataattributes for the Zisk loader, removes EH frames, trims.bss. Only runs for--libc zisk. -
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.
Runtime entry point. Calls RhInitialize, registers the managed-code range, runs module initialisers, jumps to __managed__Main.
Platform abstraction. getenv, sched_*, mmap, malloc/realloc/free, syscall — wrapped or routed to a downward bump allocator.
Object allocators on top of calloc, EventPipe stubs, default-locale, lock no-ops, custom RhpCidResolve, thread-static storage.
Hand-written RISC-V64 asm: write-without-write-barrier ref assignment and the RhpCidResolve trampoline.
Minimal thread-local storage. A static 100 KiB buffer copies .tdata on first access and serves __tls_get_addr.
Empty bodies for soft-float compiler-RT helpers. Lets the binary link cleanly when the AOT pass eliminates floating point.
Deterministic LCG-based PRNG that satisfies RandomNumberGenerator and OpenSSL requests.
GSS / NetSecurity functions that all return -1. Prevents link errors for never-executed network paths.
C++ allocator shims. operator new[] and operator new forwarded to malloc.
Rust compatibility layer. sys_alloc_aligned wired to the bump allocator for adjacent precompiles.
Drop-in GC that never collects. Acceptable because each proof is short-lived with a known working set.
_start in asm + linker scripts. Sets gp, sp, tail-calls __libc_start_main.
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.
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:
# 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.