When RISC0 Met Nix
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.
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.