Build bflat from source

The full path is documented in BUILDING.md; the short version:

$ ./build.sh modules riscv64    # build all link-time modules
$ ./build.sh bflat   riscv64    # build the compiler driver
$ ./build.sh layouts riscv64    # produce the redistributable layouts

build.sh wraps dotnet build and the cross-compilation of every C / C++ / asm module under src/bflat/modules/. You need:

  • A .NET SDK — see .NET version and variant for which one.
  • clang and clang++ — they compile the C and C++ modules (cross-targeted with --target=riscv64-linux-gnu, so no separate cross compiler is needed for those).
  • The riscv64-linux-gnu binutils (as, objcopy, objdump) for the assembly modules and the layout steps, plus the gcc/g++ cross packages — which supply the sysroot clang compiles against, and are the compilers used for the module unit tests.
  • Python 3 with lief and pyelftools for the postprocessor.

A Dockerfile (Dockerfile.build) bundles all of this; run ./build_docker_image.sh once to build it, then ./docker_shell.sh to get a shell with the toolchain in place.

.NET version and variant

bflat builds against .NET 10 or .NET 11, and bundles one of two runtime lines. Both are positional arguments of build.sh:

$ ./build.sh bflat riscv64 <variant> <dotnet>
$ ./build.sh bflat riscv64 min 10        # minimal runtime, .NET 10
$ ./build.sh bflat riscv64 perf 10       # performance runtime, .NET 10
$ ./build.sh bflat riscv64 min 11        # minimal runtime, .NET 11

or, driving MSBuild directly, -p:DotnetVersion= and -p:Variant=.

min
The minimal runtime build. Smaller image, fewer prover steps to load.
perf
The performance-oriented runtime, paired with the RyuJIT knobs. Published for .NET 10 only so far; asking for it on .NET 11 fails the build with an explanation rather than downloading a tag that does not exist.

The default is .NET 11 with min. The two selectors decide the TargetFramework and the runtime release tag together, and the download cache is keyed by that tag. The mapping lives in bflat.variant.props, which is the source of truth as release tags move.

Three Docker images are published, one per supported combination:

Image Variant .NET
nethermindeth/bflat-riscv64 perf 10
nethermindeth/bflat-riscv64-min min 10
nethermindeth/bflat-riscv64-11-min min 11

About the SDK. A .NET 11 SDK builds both flavours, so one install covers compilation. Running is the other half: an SDK carries only its own major’s runtime, so a net10.0 bflat on an 11-only machine aborts with framework '10.0.0' not found unless you set DOTNET_ROLL_FORWARD=LatestMajor. The published images derive the SDK from DOTNET_VERSION and run bflat --info on the finished image, so a mismatch fails the image build instead of the first use.

Verifying a build

The build re-checks its own ILC-stage substitutions against the CoreLib it just unpacked, so a runtime bump that moves a substituted method fails here rather than at your first guest compilation:

zkVM: applied 18 C#-snippet body substitution(s)
zkVM: substitutions verified for libc=zisk against System.Private.CoreLib

The module test, fuzzing and proof suites are described on the Verification page.

Build a C# program for Zisk

$ bflat build hello.cs --os linux --libc zisk

Optional but useful flags:

Flag What it does
--arch riscv64 Set explicitly; otherwise inferred from --libc
--no-stacktrace-data Drop textual stack-trace tables. Saves significant binary size.
--remove-eh Strip the DWARF unwind tables: ~100 KiB smaller, but a throw exits the guest instead of running catch/finally.
--no-globalization Forced on for zisk / zisk_sim; listed for clarity.
-Os / -Ot Optimise for size or speed. zkVMs reward size — every prover-step counts.
--mstat Emit MSTAT and DGML files for dotnet-stat size analysis.
--symchart After linking, run readelf and produce an HTML symbol-size chart.
--error-on-float Fail the build if any emitted method’s IL contains a floating-point conversion. Known-dead sites are listed and reported as warnings.
--error-on-float-binary Fail if the linked binary contains any F/D instruction. Catches what the IL scan cannot see — comparisons, and native objects.
--error-on-compressed Fail if the linked binary contains a compressed (C) instruction.
--error-on-atomic Fail if the linked binary contains an atomic (A) instruction: lr/sc, amo*.
--substitution <file> Apply an extra ILLink substitutions file on top of the built-in zkVM set.
-x Print the ILC and linker commands as they run.

For --libc zisk the link produces <output> and the postprocessor then writes <output>.patched beside it — .patched is the file Zisk runs, and the one to ship; the unpatched ELF is kept because it is the more convenient thing to disassemble. For --libc zisk_sim there is no postprocessing step and the single output runs under qemu-riscv64 or natively on RISC-V64 Linux.

Run a built binary

# Native RISC-V64 host
$ ./hello

# x86 host with QEMU user-mode
$ qemu-riscv64 ./hello

# Inside Zisk's emulator — note it is the postprocessed ELF, passed with -e
# (--rom takes an already-converted ROM, not an ELF)
$ ziskemu -e ./hello.patched

Linking external libraries via NuGet

bflat understands --extlib arguments that point at NuGet packages. Three forms are accepted:

$ bflat build app.cs --os linux --libc zisk \
    --extlib repo:version          # GitHub release with a single .nupkg attachment
    --extlib path/to/package.nupkg # local nupkg
    --extlib path/to/package.bflat.manifest   # local manifest, sources resolved relatively

Every package must contain a *.bflat.manifest JSON file at its zip root:

{
  "name": "libziskos",
  "package_version": "1.0.0",
  "builds": [
    {
      "arch":   "riscv64",
      "os":     "linux",
      "libc":   "zisk",
      "static_lib":          "runtimes/linux-riscv64/native/libziskos.a",
      "dotnet_lib":          "lib/net10.0/Nethermind.ZiskBindings.dll",
      "dotnet_assemblyname": "Nethermind.ZiskBindings"
    }
  ]
}

bflat picks the entry whose arch / os / libc triple matches the build target. The static library is added to the link line; the .NET assembly is referenced and AOT-compiled along with the user code. Paths are relative to the manifest.

The canonical example is bflat-libziskos, which exposes Zisk’s precompile API to managed code.

Targeting the simulator

$ bflat build app.cs --os linux --libc zisk_sim

Same runtime, allocator and TLS shim as the Zisk target, but without the ELF postprocessor and with a slightly looser linker script — the build to reach for when debugging under GDB.

Known limitations

  • Multi-threading. There is exactly one thread of execution. Locks are no-ops, pthread_create returns success without doing anything. Code that relies on parallel progress will deadlock or misbehave.
  • Filesystem and console. open, __stdio_write, and __stdio_read all return failure. Programs must do their I/O through whatever the zkVM provides (in Zisk’s case, the precompile API).
  • Time and randomness are deterministic. clock_gettime returns -1, and the RNG entry points are answered by a fixed-sequence PRNG (see the rng_stupid module) — the same build produces the same bytes on every run, which is what a reproducible proof requires.
  • Exceptions cover explicit throw only. try/catch/finally, filters and rethrow work; an uncaught exception ends in FailFast. A hardware fault — a null dereference, say — kills the guest with SIGSEGV instead of surfacing as an exception: nothing installs a signal handler and the emulator raises no trap.