When RISC0 Met Nix

Marijan
Casper Association R & D
6 min readFeb 7, 2024

Getting started on developing zero-knowledge applications can be challenging in the beginning as you are probably required to learn new concepts and explore different ideas and approaches. Not to mention integrating your finished project with other applications and deploying it. It’s fair to say that this can put beginner and experienced developers in quite challenging situations.

A developer who is just getting started is not interested in dealing with setting up the development environment by manually running an endless list of imperative steps and solving problems that occur along the way before writing a single line of code.
On the other end, an experienced developer who wants to share what was built is not interested in thinking of “How do I make sure that the machines of my users running my code will have the same RISC0 virtual machine installed that I used during development and testing”. Moreover, the developer doesn’t want to resolve build issues remotely after sharing the code with a coworker and getting the response “It does not work on my machine”.

To serve both developers who want to try out RISC0 and developers who want to reliably and reproducibly deploy their applications, we packaged all the RISC0-related dependencies with Nix and open-sourced the package set called risc0pkgs together with a utility function called buildRisc0Package for the rescue.

In this blog post, we want to show you how you can start developing your first RISC0 project that is packaged with Nix.

Note: If you encounter any errors or difficulties please open up a new issue here or a new discussion here.

Nix packages RISC0

Start With the Flake Template

First, create a new directory:

$ mkdir risczero

Now go to the directory, initialize the project with the risc0pkgs template using the nix command, and initialize a new git repository.

$ cd risczero
$ nix flake init -t github:cspr-rad/risc0pkgs#default

$ git init
$ git add -A

The last command that stages all the files in the repository is crucial when working with Nix. Nix will only consider staged files during a build to ensure you are not missing any files required to build your project.

Using the nix command, we can now explore what buildable outputs our project is exposing. When we run this command the first time we will be asked whether we want to trust the risc0pkgs binary cache configured in our Flake. Trusting this setting allows us to download pre-built binaries of rustc and r0vm instead of building them from source. To do so, run:

$ nix flake show
warning: creating lock file '/home/user/risczero/flake.lock'
do you want to allow configuration setting 'extra-substituters' to be set to 'https://risc0pkgs.cachix.org' (y/N)? y
do you want to permanently mark this value as trusted (y/N)? y
do you want to allow configuration setting 'extra-trusted-public-keys' to be set to 'cspr.cachix.org-1:vEZlmbOsmTXkmEi4DSdqNVyq25VPNpmSm6qCs4IuTgE=' (y/N)? y
do you want to permanently mark this value as trusted (y/N)? y
├───devShells
│ ├───aarch64-darwin
│ │ └───default omitted (use '--all-systems' to show)
│ ├───x86_64-darwin
│ │ └───default omitted (use '--all-systems' to show)
│ └───x86_64-linux
│ └───default: development environment 'nix-shell'
└───packages
├───aarch64-darwin
│ └───risc0package omitted (use '--all-systems' to show)
├───x86_64-darwin
│ └───risc0package omitted (use '--all-systems' to show)
└───x86_64-linux
└───risc0package: package 'risc0package-0.0.1'

We can see that we have a default development shell and a package called risc0package for the platforms aarch64-darwin, x86_64-darwin and x86_64-linux available to be build.

To build the package for our current platform, run:

$ nix build .#risc0package

The nix command will detect your current platform and build the packages.<platform>.risc0package output and create a symlink called result. The result symlink points to a Nix store path that contains the host executable. If you run the executable, nothing interesting will happen since we are proving and verifying the execution of empty guest code.

Now that we are sure that we can build the template code using Nix, we can start implementing some functionality. It’s recommended to enter a development shell and use Rust’s toolchain directly during development. To enter a development shell, run:

$ nix develop .#default

Alternatively, you can run nix develop. The command above serves the purpose of uncluttering the magic the Nix command does.

Inside a development shell, we can run cargo as usual i.e.

$ cargo build

One last important thing to do after you finish implementing your functionality is to trigger another build using Nix:

$ nix build .#risc0package
warning: Git tree '/home/user/risczero' is dirty
error: builder for '/nix/store/k1vidjcbh1gqhxcj60h5jbhdmwm4764l-risc0package-0.0.1.drv' failed with exit code 1;
last 10 log lines:
> ERROR: cargoHash or cargoSha256 is out of date
>
> Cargo.lock is not the same in /build/risc0package-0.0.1-vendor.tar.gz
>
> To fix the issue:
> 1. Set cargoHash/cargoSha256 to an empty string: `cargoHash = "";`
> 2. Build the derivation and wait for it to fail with a hash mismatch
> 3. Copy the "got: sha256-..." value back into the cargoHash field
> You should have: cargoHash = "sha256-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=";
>
For full logs, run 'nix log /nix/store/k1vidjcbh1gqhxcj60h5jbhdmwm4764l-risc0package-0.0.1.drv'.

You should expect this error message saying ERROR: cargoHash or cargoSha256 is out of date. What we need to do now is edit our flake.nix, change the line cargoSha256 = “sha256-oY52S/Yljkn9lfH8oA8+XkCwAwOaOBzIT5uLCMZZYxI=”; to cargoSha256 = “”;, and build again:

$ nix build .#risc0package
warning: Git tree '/home/user/risczero' is dirty
warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
[1/0/2 built, 0.0 MiB DL] building risc0package-0.0.1-vendor.tar.gz (installPhase): Runni[1/0/2 built, 0.0 MiB DL] building risc0package-0.0.1-vendor.tar.gz (installPhase): Runni[1error: hash mismatch in fixed-output derivation '/nix/store/l35w4h80ikfprmjpjaq7w5h2zvcnv3qv-risc0package-0.0.1-vendor.tar.gz.drv':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-S7ynK+boJ6WYFzQaLyws7fYvKefKdWkL/pHtE0q431Y=
error: 1 dependencies of derivation '/nix/store/samfchmwky83jrnbfpmry1svbzhr0ddr-risc0package-0.0.1.drv' failed to build

As you can see, Nix will then suggest you the new cargoSha256. We can now update the hash in the flake.nix, and make sure it works by running:

$ nix build .#risc0package

The somewhat laborious update of the cargoSha256 is necessary to ensure reproducibility and is only required when we are done modifying the source and ready to commit and push our code.

flake.nix Walkthrough

Next, let us walk through the template’s flake.nix to get a better understanding of how to use risc0pkgs (You can find this file upstream here).

{
description = "risc0 project template";

nixConfig = {
extra-substituters = [
"https://risc0pkgs.cachix.org"
];
extra-trusted-public-keys = [
"risc0pkgs.cachix.org-1:EY5UazX0/Q7hGCm6xQSgKX6UkpzyOf07pxjfhhRK7kE="
];
};

inputs = {
nixpkgs.follows = "risc0pkgs/nixpkgs";
risc0pkgs.url = "github:cspr-rad/risc0pkgs";
};

outputs = { self, nixpkgs, risc0pkgs }:
let
systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSystem = nixpkgs.lib.genAttrs systems;
in
{
packages = forEachSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
risc0package =
risc0pkgs.lib.${system}.buildRisc0Package {
pname = "risc0package";
version = "0.0.1";
src = ./.;
doCheck = false;
cargoSha256 = "sha256-oY52S/Yljkn9lfH8oA8+XkCwAwOaOBzIT5uLCMZZYxI=";
nativeBuildInputs = [ pkgs.makeWrapper ];
postInstall = ''
wrapProgram $out/bin/host \
--set PATH ${pkgs.lib.makeBinPath [ risc0pkgs.packages.${system}.r0vm ]}
'';
};
});

devShells = forEachSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.mkShell {
RISC0_DEV_MODE = 1;
inputsFrom = [ self.packages.${system}.risc0package ];
nativeBuildInputs = [ risc0pkgs.packages.${system}.r0vm ];
};
});
};
}

Let’s start with the top-most attributes. Every Flake requires two attributes called inputs and outputs. The inputs are the dependencies of our Flake, in this case nixpkgs and risc0pkgs. The nixpkgs input is configured to follow risc0pkgs input of nixpkgs. There is also an attribute called nixConfig where we configured the previously mentioned binary-cache.

In the packages outputs, we define a utility function called forEachSystem, which I recommend replacing with flake-parts if your projects Nix code should get bigger and more complicated.

Further down we define a packages output with a nested risc0package. forEachSystem is used to create an output of the form packages.<system>.risc0package.
In risc0package we use the buildRisc0Package library function to package our code. This function calls buildRustPackage under the hood, and sets up the right dependencies needed to compile your RISC0 host and guest code.

The important and interesting definitions are nativeBuildInputs = [ pkgs.makeWrapper ]; and the postInstall hook.wrapPogram is a utility coming from makeWrapper that takes a binary and ensures that this binary has the defined PATH variable configured wherever you deploy the risc0package. This means, that wherever you distribute the closure of risc0package the r0vm will be distributed with it, and it will have it available in its environment.

--

--