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.
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 adaptations
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 — driver, 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, 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.
ILC substitutions · link-time modules · ELF postprocess
dotnet/dotnet at a release branch, built with upstream's own build system
The minimal profile is riscv64 correctness fixes only — each one meant for upstream
10 and 11, each with its own versioned profile
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.
-
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
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 and 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 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.
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.
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.
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
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.
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:
# 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.