diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4ecdb6438..ce50242bf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,24 +46,19 @@ jobs: with: persist-credentials: false - - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 - - name: Install repro-env - run: | - wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' - echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - - sudo install -m755 repro-env -t /usr/bin + - name: Restore /nix from cache + uses: ./.github/actions/restore-nix-cache - - name: Build MPC Node binary and image + - name: Build MPC Node image and load into Docker run: | - export NODE_IMAGE_NAME=test_image_tag_ci - ./deployment/build-images.sh --node + archive=$(nix build --no-link --print-out-paths .#node-image) + docker load < "$archive" - name: Check mpc-node docker image can initialize and start - run: | - export NODE_IMAGE_NAME=test_image_tag_ci - ./scripts/check-mpc-node-docker-starts.sh + run: ./scripts/check-mpc-node-docker-starts.sh docker-rust-launcher-build-and-verify: name: "Build Rust Launcher Docker image and verify" @@ -80,29 +75,24 @@ jobs: with: persist-credentials: false - - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y skopeo liblzma-dev podman + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 - - name: Install repro-env - run: | - wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' - echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - - sudo install -m755 repro-env -t /usr/bin + - name: Restore /nix from cache + uses: ./.github/actions/restore-nix-cache - name: Build Rust launcher docker image and verify its hash shell: bash + run: ./scripts/build-and-verify-rust-launcher-docker-image.sh + + - name: Load launcher image into Docker run: | - ./scripts/build-and-verify-rust-launcher-docker-image.sh + archive=$(nix build --no-link --print-out-paths .#rust-launcher-image) + docker load < "$archive" - name: Run Rust launcher non-TEE runtime check shell: bash - run: | - ./scripts/check-mpc-node-docker-starts.sh --rust-launcher + run: ./scripts/check-mpc-node-docker-starts.sh --rust-launcher mpc-unittests: name: "Cargo test: ${{ matrix.group }}" diff --git a/.github/workflows/docker_build_node.yml b/.github/workflows/docker_build_node.yml index 0960ff7c48..25eef03000 100644 --- a/.github/workflows/docker_build_node.yml +++ b/.github/workflows/docker_build_node.yml @@ -15,7 +15,7 @@ on: jobs: build-and-push-images: name: "Build and push Docker node image with commit hash" - runs-on: warp-ubuntu-2404-x64-16x + runs-on: warp-ubuntu-2404-x64-32x environment: production permissions: contents: read @@ -26,26 +26,15 @@ jobs: with: persist-credentials: false + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 + - name: Login to Docker Hub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PAT }} - - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - - - name: Install repro-env - run: | - wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' - echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - - sudo install -m755 repro-env -t /usr/bin - - - name: Install skopeo - run: | - sudo apt-get update - sudo apt-get install -y skopeo - - name: Build and push node image run: | export NODE_IMAGE_NAME=mpc-node diff --git a/.github/workflows/docker_build_node_gcp.yml b/.github/workflows/docker_build_node_gcp.yml index e081f145bc..49e2dbb853 100644 --- a/.github/workflows/docker_build_node_gcp.yml +++ b/.github/workflows/docker_build_node_gcp.yml @@ -15,7 +15,7 @@ on: jobs: build-and-push-images: name: "Build and push Docker node gcp image with commit hash" - runs-on: warp-ubuntu-2404-x64-16x + runs-on: warp-ubuntu-2404-x64-32x environment: production permissions: contents: read @@ -26,26 +26,15 @@ jobs: with: persist-credentials: false + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 + - name: Login to Docker Hub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PAT }} - - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - - - name: Install repro-env - run: | - wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' - echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - - sudo install -m755 repro-env -t /usr/bin - - - name: Install skopeo - run: | - sudo apt-get update - sudo apt-get install -y skopeo - - name: Build and push node gcp image run: | export NODE_GCP_IMAGE_NAME=mpc-node-gcp diff --git a/.github/workflows/docker_build_rust_launcher.yml b/.github/workflows/docker_build_rust_launcher.yml index 59f58ed44d..a4914475e5 100644 --- a/.github/workflows/docker_build_rust_launcher.yml +++ b/.github/workflows/docker_build_rust_launcher.yml @@ -15,7 +15,7 @@ on: jobs: build-and-push-images: name: "Build and push Rust launcher Docker image with commit hash" - runs-on: warp-ubuntu-2404-x64-8x + runs-on: warp-ubuntu-2404-x64-32x environment: production permissions: contents: read @@ -26,19 +26,8 @@ jobs: with: persist-credentials: false - - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y skopeo liblzma-dev podman - - - name: Install repro-env - run: | - wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' - echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - - sudo install -m755 repro-env -t /usr/bin + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17 - name: Login to Docker Hub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 diff --git a/README.md b/README.md index 64b02041c1..b160148970 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ cargo install cargo-insta ## Reproducible Builds -Both the node and launcher Docker images support reproducible builds, ensuring identical binaries from the same source. Run `./deployment/build-images.sh` from the project root. +Both the node and launcher Docker images support reproducible builds, ensuring identical binaries from the same source. The build is driven by Nix: run `./deployment/build-images.sh` from the project root, or invoke the derivations directly — `nix build .#node-image` produces an OCI tarball and `nix build .#node-image-manifest-digest` prints the digest operators vote for. -For prerequisites and options, see [docs/reproducible-builds.md](docs/reproducible-builds.md). +For prerequisites and the full set of derivations, see [docs/reproducible-builds.md](docs/reproducible-builds.md). ## Releases diff --git a/deployment/Dockerfile-node b/deployment/Dockerfile-node deleted file mode 100644 index 670547443c..0000000000 --- a/deployment/Dockerfile-node +++ /dev/null @@ -1,21 +0,0 @@ -FROM debian:bookworm-slim@sha256:acd98e6cfc42813a4db9ca54ed79b6f702830bfc2fa43a2c2e87517371d82edb -ENV DEBIAN_FRONTEND=noninteractive - -RUN \ - --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ - repro-sources-list.sh && \ - apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates openssl python3 && \ - : "Clean up for improving reproducibility" && \ - rm -rf /var/log/* /var/cache/ldconfig/aux-cache - -ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -WORKDIR /app -COPY target/reproducible/mpc-node mpc-node -# The chmod part is also necessary for reproducibility -COPY --chmod=0755 deployment/start.sh /app/start.sh -# Copy embedded localnet genesis file -COPY --chmod=0755 deployment/localnet/genesis.json /app/localnet-genesis.json -CMD [ "/app/start.sh" ] diff --git a/deployment/Dockerfile-node-gcp b/deployment/Dockerfile-node-gcp deleted file mode 100644 index 6180ab5925..0000000000 --- a/deployment/Dockerfile-node-gcp +++ /dev/null @@ -1,21 +0,0 @@ -FROM google/cloud-sdk:debian_component_based@sha256:666e10130e66fa3146e3be5a27fdba9fd6006131bebf6bd63e62fcffe04aa901 -ENV DEBIAN_FRONTEND=noninteractive - -RUN \ - --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ - repro-sources-list.sh && \ - apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates openssl python3 && \ - : "Clean up for improving reproducibility" && \ - rm -rf /var/log/* /var/cache/ldconfig/aux-cache - -ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -WORKDIR /app -COPY target/reproducible/mpc-node mpc-node -# The chmod part is also necessary for reproducibility -COPY --chmod=0755 deployment/start.sh /app/start.sh -# Copy embedded localnet genesis file -COPY --chmod=0755 deployment/localnet/genesis.json /app/localnet-genesis.json -CMD [ "/app/start.sh" ] diff --git a/deployment/Dockerfile-rust-launcher b/deployment/Dockerfile-rust-launcher deleted file mode 100644 index 172609cdd4..0000000000 --- a/deployment/Dockerfile-rust-launcher +++ /dev/null @@ -1,18 +0,0 @@ -FROM debian:trixie-slim@sha256:26f98ccd92fd0a44d6928ce8ff8f4921b4d2f535bfa07555ee5d18f61429cf0c -ENV DEBIAN_FRONTEND=noninteractive - -RUN \ - --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ - repro-sources-list.sh && \ - apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates docker-cli docker-compose && \ - : "Clean up for improving reproducibility" && \ - rm -rf /var/log/* /var/cache/ldconfig/aux-cache - -ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -WORKDIR /app -COPY --chmod=0755 target/reproducible/tee-launcher tee-launcher -RUN mkdir -p /app-data && mkdir -p /mnt/shared -CMD ["/app/tee-launcher"] diff --git a/deployment/build-images.sh b/deployment/build-images.sh index 594c7593cf..f71aaf48d0 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -1,15 +1,22 @@ #! /usr/bin/env bash -# Script to reproducibly build the docker images for the node and launcher +# Script to reproducibly build the docker images for the node and launcher. # -# Requirements: docker, docker-buildx, git, find, touch, skopeo -# Extra requirements if using --node or --rust-launcher: repro-env, podman -# Extra requirements if using --push: docker must be logged in to registry +# The heavy lifting happens in Nix: each image is a `dockerTools` derivation +# (see nix/*-image.nix) and each has a companion `*-manifest-digest` +# derivation that computes the registry manifest digest deterministically +# inside the Nix sandbox. This script is a thin wrapper that builds the +# requested derivations, prints the binary hashes and manifest digests, and +# optionally pushes the images. +# +# Requirements: nix (with flakes enabled), git — nothing else; skopeo is +# run through `nix run .#skopeo`. +# Extra requirements if using --push: logged in to the registry, e.g. +# nix run .#skopeo -- login docker.io # # Usage: # ./deployment/build-images.sh [--node] [--node-gcp] [--rust-launcher] [--push] # If no image flags are used, all images are built -# Manifest digests are always computed and printed (skopeo required) - +# Manifest digests are always computed and printed set -euo pipefail @@ -35,7 +42,7 @@ do ;; *) echo "Unknown parameter: $arg" - echo "Usage: $0 [--node] [--rust-launcher] [--push]" + echo "Usage: $0 [--node] [--node-gcp] [--rust-launcher] [--push]" exit 1 ;; esac @@ -60,141 +67,52 @@ require_cmds() { [[ "${missing}" -eq 0 ]] || die "Please install the missing dependencies above." } -require_cmds docker git find touch skopeo - -if $USE_NODE || $USE_RUST_LAUNCHER; then - require_cmds repro-env podman -fi - -if ! docker buildx &>/dev/null; then - die "Please install docker-buildx" -fi +require_cmds nix git if [ ! "$(pwd)" = "$(git rev-parse --show-toplevel)" ]; then - echo "Must be called from project root!" - exit 1 + die "Must be called from project root!" fi -DOCKERFILE_NODE=deployment/Dockerfile-node : "${NODE_IMAGE_NAME:=mpc-node}" - -DOCKERFILE_NODE_GCP=deployment/Dockerfile-node-gcp : "${NODE_GCP_IMAGE_NAME:=mpc-node-gcp}" - -DOCKERFILE_RUST_LAUNCHER=deployment/Dockerfile-rust-launcher : "${RUST_LAUNCHER_IMAGE_NAME:=mpc-rust-launcher}" - -SOURCE_DATE_EPOCH=0 GIT_COMMIT_HASH=$(git rev-parse HEAD) -# This might be necessary to fix reproducibility with old docker versions where -# rewrite-timestamp is not working as expected -# https://github.com/moby/buildkit/issues/4986 -find . \( -type f -o -type d \) -exec touch -d @"$SOURCE_DATE_EPOCH" {} + - -# Create our own builder (build env) to enable reproducible images - -buildkit_version="0.27.1" -buildkit_image_name="buildkit_${buildkit_version}" - -if ! docker buildx inspect ${buildkit_image_name} &>/dev/null; then - docker buildx create --use --driver-opt image=moby/buildkit:v${buildkit_version} --name ${buildkit_image_name} -fi - - -build_reproducible_image() { - local image_name=$1 - local dockerfile_path=$2 - docker buildx build --builder "${buildkit_image_name}" --no-cache \ - --build-arg SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \ - --output "type=docker,name=$image_name,rewrite-timestamp=true" \ - --progress plain -f "$dockerfile_path" . +# Build a flake attribute and print its store output path. Build logs go to +# stderr; only the path lands on stdout. +nix_out() { + nix build --no-link --print-out-paths ".#$1" } -# Compress a locally built image via skopeo to a temp directory. -# Prints the temp dir path to stdout. The manifest digest can be -# computed from $dir/manifest.json. -skopeo_compress() { - local image_name="$1" - local td - td=$(mktemp -d) - # Compress the built image to a local directory, which implicitly computes - # the manifest digest in $td/manifest.json - skopeo copy --all --dest-compress "docker-daemon:${image_name}:latest" "dir:$td" >&2 - echo "$td" +# The flake-pinned skopeo — the same one image-dir.nix used to produce the +# layouts being pushed, and no system install needed. +skopeo() { + nix run .#skopeo -- "$@" } -manifest_digest_from_dir() { - echo "sha256:$(sha256sum "$1/manifest.json" | cut -d' ' -f1)" -} - -if $USE_RUST_LAUNCHER; then - SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH repro-env build --env SOURCE_DATE_EPOCH -- cargo build -p tee-launcher --profile reproducible --locked - rust_launcher_binary_hash=$(sha256sum target/reproducible/tee-launcher | cut -d' ' -f1) - - build_reproducible_image "$RUST_LAUNCHER_IMAGE_NAME" "$DOCKERFILE_RUST_LAUNCHER" - rust_launcher_skopeo_dir="$(skopeo_compress "$RUST_LAUNCHER_IMAGE_NAME")" - rust_launcher_manifest_digest="$(manifest_digest_from_dir "$rust_launcher_skopeo_dir")" -fi - if $USE_NODE || $USE_NODE_GCP; then - # Pin jemalloc's `./configure` auto-detected values so tikv-jemalloc-sys - # produces identical bytes across builders. See nix/mpc-node.nix for the - # full rationale; values match the standard x86_64 Linux ABI. - # - # GIT_CEILING_DIRECTORIES stops jemalloc's `./configure` from walking out - # of `target/` and finding the surrounding mpc repo's `.git/` — without - # it, `git describe HEAD` returns mpc's commit SHA, which is then baked - # into jemalloc's VERSION file (and the linked binary's `.rodata` and - # `smallocx_` exported symbol). The path is the in-container - # workspace mount (`/build`), not the host path. - # Pin the C/C++ ISA for cc-crate dependencies (rocksdb, snappy, zstd, - # jemalloc, ...) to match the rustc target-cpu set in .cargo/config.toml. - # Without this, the cc crate uses the container's default `-march`, which - # would diverge from the Rust code's ISA expectations. - # - # PCLMUL and AES are not part of the v3 micro-arch level (per System V - # psABI) but are universally available on v3-capable hardware. Adding - # them explicitly keeps rocksdb's PCLMUL-accelerated CRC32C path - # compiled in. Match nix/mpc-node.nix and flake.nix. - # BUILT_OVERRIDE_mpc_node_GIT_VERSION pins built's GIT_VERSION so local git - # tags aren't embedded in the binary (which would break reproducibility). - SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH \ - JEMALLOC_SYS_WITH_LG_VADDR=48 \ - JEMALLOC_SYS_WITH_LG_PAGE=12 \ - JEMALLOC_SYS_WITH_LG_HUGEPAGE=21 \ - GIT_CEILING_DIRECTORIES=/build/target \ - BUILT_OVERRIDE_mpc_node_GIT_VERSION="${GIT_COMMIT_HASH:0:7}" \ - CFLAGS="-march=x86-64-v3 -mpclmul -maes" \ - CXXFLAGS="-march=x86-64-v3 -mpclmul -maes" \ - repro-env build \ - --env SOURCE_DATE_EPOCH \ - --env JEMALLOC_SYS_WITH_LG_VADDR \ - --env JEMALLOC_SYS_WITH_LG_PAGE \ - --env JEMALLOC_SYS_WITH_LG_HUGEPAGE \ - --env GIT_CEILING_DIRECTORIES \ - --env BUILT_OVERRIDE_mpc_node_GIT_VERSION \ - --env CFLAGS \ - --env CXXFLAGS \ - -- cargo build -p mpc-node --profile reproducible --locked - node_binary_hash=$(sha256sum target/reproducible/mpc-node | cut -d' ' -f1) + node_binary_hash=$(sha256sum "$(nix_out mpc-node)/bin/mpc-node" | cut -d' ' -f1) fi if $USE_NODE; then - build_reproducible_image "$NODE_IMAGE_NAME" "$DOCKERFILE_NODE" - node_skopeo_dir="$(skopeo_compress "$NODE_IMAGE_NAME")" - node_manifest_digest="$(manifest_digest_from_dir "$node_skopeo_dir")" + node_image_dir=$(nix_out node-image-dir) + node_manifest_digest=$(cat "$(nix_out node-image-manifest-digest)") fi if $USE_NODE_GCP; then - build_reproducible_image "$NODE_GCP_IMAGE_NAME" "$DOCKERFILE_NODE_GCP" - node_gcp_skopeo_dir="$(skopeo_compress "$NODE_GCP_IMAGE_NAME")" - node_gcp_manifest_digest="$(manifest_digest_from_dir "$node_gcp_skopeo_dir")" + node_gcp_image_dir=$(nix_out node-gcp-image-dir) + node_gcp_manifest_digest=$(cat "$(nix_out node-gcp-image-manifest-digest)") +fi + +if $USE_RUST_LAUNCHER; then + rust_launcher_binary_hash=$(sha256sum "$(nix_out tee-launcher)/bin/tee-launcher" | cut -d' ' -f1) + rust_launcher_image_dir=$(nix_out rust-launcher-image-dir) + rust_launcher_manifest_digest=$(cat "$(nix_out rust-launcher-image-manifest-digest)") fi if $USE_PUSH; then - # This assumes that docker is logged-in dockerhub registry with nearone user + # This assumes that skopeo is logged-in to the dockerhub registry with the nearone user branch_name=$(git branch --show-current) if [ -z "$branch_name" ]; then @@ -209,22 +127,25 @@ if $USE_PUSH; then image_tag="$sanitized_branch_name-$short_hash" echo "Using branch-hash tag: $image_tag" - # Push from the already-compressed local directory, preserving the manifest digest. + # Push the Nix-built `dir:` layouts. Their blobs are already compressed + # and their manifest is exactly what the `*-manifest-digest` derivations + # hashed, so with `--preserve-digests` the digest that lands in the + # registry is guaranteed to be the one printed below — independent of + # the skopeo version doing the pushing. if $USE_NODE; then - skopeo copy --preserve-digests "dir:$node_skopeo_dir" "docker://docker.io/nearone/$NODE_IMAGE_NAME:$image_tag" + skopeo copy --preserve-digests "dir:$node_image_dir" "docker://docker.io/nearone/$NODE_IMAGE_NAME:$image_tag" fi if $USE_NODE_GCP; then - skopeo copy --preserve-digests "dir:$node_gcp_skopeo_dir" "docker://docker.io/nearone/$NODE_GCP_IMAGE_NAME:$image_tag" + skopeo copy --preserve-digests "dir:$node_gcp_image_dir" "docker://docker.io/nearone/$NODE_GCP_IMAGE_NAME:$image_tag" fi if $USE_RUST_LAUNCHER; then - skopeo copy --preserve-digests "dir:$rust_launcher_skopeo_dir" "docker://docker.io/nearone/$RUST_LAUNCHER_IMAGE_NAME:$image_tag" + skopeo copy --preserve-digests "dir:$rust_launcher_image_dir" "docker://docker.io/nearone/$RUST_LAUNCHER_IMAGE_NAME:$image_tag" fi fi echo "commit hash: $GIT_COMMIT_HASH" -echo "SOURCE_DATE_EPOCH used: $SOURCE_DATE_EPOCH" if $USE_NODE || $USE_NODE_GCP; then echo "node binary hash: $node_binary_hash" fi diff --git a/deployment/repro-sources-list.sh b/deployment/repro-sources-list.sh deleted file mode 100755 index ce0242d582..0000000000 --- a/deployment/repro-sources-list.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# -# Copyright The repro-sources-list.sh Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# ----------------------------------------------------------------------------- -# repro-sources-list.sh: -# configures /etc/apt/sources.list and similar files for installing packages from a snapshot. -# -# This script is expected to be executed inside Dockerfile. -# -# The following distributions are supported: -# - debian:11 (/etc/apt/sources.list) -# - debian:12 (/etc/apt/sources.list.d/debian.sources) -# - ubuntu:22.04 (/etc/apt/sources.list) -# - ubuntu:24.04 (/etc/apt/sources.listd/ubuntu.sources) -# - archlinux (/etc/pacman.d/mirrorlist) -# -# For the further information, see https://github.com/reproducible-containers/repro-sources-list.sh -# ----------------------------------------------------------------------------- - -set -eux -o pipefail - -. /etc/os-release - -: "${KEEP_CACHE:=0}" - -keep_apt_cache() { - rm -f /etc/apt/apt.conf.d/docker-clean - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache -} - -case "${ID}" in -"debian") - : "${SNAPSHOT_ARCHIVE_BASE:=http://snapshot.debian.org/archive/}" - : "${BACKPORTS:=}" - if [ -e /etc/apt/sources.list.d/debian.sources ]; then - : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /etc/apt/sources.list.d/debian.sources)}" - rm -f /etc/apt/sources.list.d/debian.sources - else - : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /etc/apt/sources.list)}" - fi - snapshot="$(printf "%(%Y%m%dT%H%M%SZ)T\n" "${SOURCE_DATE_EPOCH}")" - # TODO: use the new format for Debian >= 12 - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}debian/${snapshot} ${VERSION_CODENAME} main" >/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}debian-security/${snapshot} ${VERSION_CODENAME}-security main" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}debian/${snapshot} ${VERSION_CODENAME}-updates main" >>/etc/apt/sources.list - if [ "${BACKPORTS}" = 1 ]; then echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}debian/${snapshot} ${VERSION_CODENAME}-backports main" >>/etc/apt/sources.list; fi - if [ "${KEEP_CACHE}" = 1 ]; then keep_apt_cache; fi - ;; -"ubuntu") - : "${SNAPSHOT_ARCHIVE_BASE:=http://snapshot.ubuntu.com/}" - if [ -e /etc/apt/sources.list.d/ubuntu.sources ]; then - : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /etc/apt/sources.list.d/ubuntu.sources)}" - rm -f /etc/apt/sources.list.d/ubuntu.sources - else - : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /etc/apt/sources.list)}" - fi - snapshot="$(printf "%(%Y%m%dT%H%M%SZ)T\n" "${SOURCE_DATE_EPOCH}")" - # TODO: use the new format for Ubuntu >= 24.04 - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME} main restricted" >/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-updates main restricted" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME} universe" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-updates universe" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME} multiverse" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-updates multiverse" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-security main restricted" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-security universe" >>/etc/apt/sources.list - echo "deb [check-valid-until=no] ${SNAPSHOT_ARCHIVE_BASE}ubuntu/${snapshot} ${VERSION_CODENAME}-security multiverse" >>/etc/apt/sources.list - if [ "${KEEP_CACHE}" = 1 ]; then keep_apt_cache; fi - # http://snapshot.ubuntu.com is redirected to https, so we have to install ca-certificates - export DEBIAN_FRONTEND=noninteractive - apt-get -o Acquire::https::Verify-Peer=false update >&2 - apt-get -o Acquire::https::Verify-Peer=false install -y ca-certificates >&2 - ;; -"arch") - : "${SNAPSHOT_ARCHIVE_BASE:=http://archive.archlinux.org/}" - : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /var/log/pacman.log)}" - export SOURCE_DATE_EPOCH - # shellcheck disable=SC2016 - date -d "@${SOURCE_DATE_EPOCH}" "+Server = ${SNAPSHOT_ARCHIVE_BASE}repos/%Y/%m/%d/\$repo/os/\$arch" >/etc/pacman.d/mirrorlist - ;; -*) - echo >&2 "Unsupported distribution: ${ID}" - exit 1 - ;; -esac - -: "${WRITE_SOURCE_DATE_EPOCH:=/dev/null}" -echo "${SOURCE_DATE_EPOCH}" >"${WRITE_SOURCE_DATE_EPOCH}" -echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" diff --git a/docs/localnet/docker-localnet.md b/docs/localnet/docker-localnet.md index 87db4c163a..2760afdb2f 100644 --- a/docs/localnet/docker-localnet.md +++ b/docs/localnet/docker-localnet.md @@ -5,7 +5,8 @@ Brain dump right now, might clean this up for reproducibility. # Step 1: Build docker images ```shell -deployment/build-images.sh --node +# Produces an OCI tarball at result/. Load it into the local docker daemon: +nix build .#node-image && docker load < result ``` # Step 2: Start localnet diff --git a/docs/reproducible-builds.md b/docs/reproducible-builds.md index 27f654fdf1..6c10b95a41 100644 --- a/docs/reproducible-builds.md +++ b/docs/reproducible-builds.md @@ -5,47 +5,72 @@ images, and for the on-chain MPC contract WASM. Reproducible builds ensure that the same source code always produces identical binaries, which is important for security and verification purposes. -## Prerequisites +The image builds are driven entirely by Nix. The flake's package set produces both the bare binaries (verifiably bit-for-bit reproducible across builders) and the OCI image tarballs that wrap them. -**Common requirements** (for both node and launcher Docker images): +## Prerequisites -- `docker` with buildx support -- `jq` +- [Nix](https://nixos.org/download/) with flakes enabled - `git` -**Additional requirements for building the node image**: - -- `repro-env` - Tool for reproducible build environments ([install here](https://github.com/kpcyrd/repro-env)) -- `podman` +No other tooling is needed: skopeo (used for pushing) is pinned in the flake +and invoked via `nix run .#skopeo`. **Requirements for building the MPC contract** (either path works): -- [Nix](https://nixos.org/download/) with flakes enabled (Nix path), or +- Nix (as above), or - `docker` and [`cargo-near`](https://github.com/near/cargo-near) (NEP-330 path) -## Building Images +## Building binaries + +```bash +nix build .#mpc-node # → result/bin/mpc-node +nix build .#tee-launcher # → result/bin/tee-launcher +``` + +The output binaries are bit-for-bit identical across builders. Hash with `sha256sum result/bin/` if you want a quick receipt. + +## Building images -The build script is located at `deployment/build-images.sh` and must be run from the project root directory. +Image derivations package the binaries above into OCI tarballs and are only available on Linux builders. -**Build both node and launcher images** (default behavior): +```bash +nix build .#node-image # general node image +nix build .#node-gcp-image # node image with google-cloud-sdk +nix build .#rust-launcher-image # tee-launcher image +``` + +The `result` symlink points to a docker-archive tarball. + +Alternatively, `deployment/build-images.sh` wraps the derivations above. Run from the project root: ```bash -./deployment/build-images.sh +./deployment/build-images.sh [--node] [--node-gcp] [--rust-launcher] [--push] ``` -**Build only the node image**: +With no image flags it builds all three. It prints the binary hashes and manifest digests for every image it builds, and with `--push` uploads them to Docker Hub (digest-preserving). This is the same entry point the release workflows use. + +## Verifying a manifest digest + +The manifest digest is the value participants vote for. Each image has a companion derivation that computes it deterministically inside the Nix sandbox: ```bash -./deployment/build-images.sh --node +nix build .#node-image-manifest-digest && cat result +# → sha256: ``` -**Build only the launcher image**: +Same for `node-gcp-image-manifest-digest` and `rust-launcher-image-manifest-digest`. Two builders running the same Nix expression at the same revision must produce the same digest. + +## Pushing to Docker Hub + +`./deployment/build-images.sh --push` pushes every image it built. To push a single image by hand, copy its `dir:` layout — the exact bytes the manifest digest was computed from — with the flake-pinned skopeo: ```bash -./deployment/build-images.sh --rust-launcher +nix run .#skopeo -- copy --preserve-digests \ + dir:$(nix build --no-link --print-out-paths .#node-image-dir) \ + docker://docker.io/nearone/mpc-node: ``` -The script will output the image hashes and other build information, which can be used to verify the reproducibility of the build. +Substitute `node-gcp-image-dir` / `rust-launcher-image-dir` and the matching destination repo as needed. Skopeo must be authenticated to the registry beforehand (`nix run .#skopeo -- login docker.io`). ## mpc-contract diff --git a/docs/running-an-mpc-node-in-tdx-external-guide.md b/docs/running-an-mpc-node-in-tdx-external-guide.md index e4d8db98fc..b66cc0edb2 100644 --- a/docs/running-an-mpc-node-in-tdx-external-guide.md +++ b/docs/running-an-mpc-node-in-tdx-external-guide.md @@ -1554,19 +1554,21 @@ cd mpc/ git checkout 828f816be36aed6f0d2438e0131b3e9d7d0931ad ``` -* Compile it using the reproduce build script. For this you need to install - `repro-env`, `docker-buildx`, and `skopeo`, and have the `docker` daemon - running. +* Build it using the reproducible build script. You need `nix` (with flakes + enabled) installed locally; no docker daemon required. ```bash $ ./deployment/build-images.sh --node ... commit hash: 828f816be36aed6f0d2438e0131b3e9d7d0931ad -SOURCE_DATE_EPOCH used: 0 node binary hash: 86c8f7d8913d6fe37a6992bba165d15a3a1d88fbf6cdff605e4827d5183721bc node manifest digest: sha256:331cfec941671ac343c52847e255eb36a280da65535d2a1e4d002c4c64686e19 ``` + See [reproducible-builds.md](reproducible-builds.md) for the underlying Nix + derivations (binaries, images, and digests) if you prefer to invoke them + directly. + The `node manifest digest` is what you vote for. When submitting the `code_hash` value in the voting command, strip the `sha256:` prefix and provide only the hex digest. The launcher pulls the image directly by this digest — Docker verifies the content matches during the pull. * Do your own due diligence on the code/binary @@ -1651,7 +1653,7 @@ For full design details, see the [CVM Upgrades section in the TEE design doc](se The following steps allow you to inspect the code used to build the launcher image and verify its manifest digest before voting. -* The launcher manifest digest is shown on DockerHub and in the reproducible build script output. To verify it, build the launcher yourself from the same commit and compare the manifest digest. +* The launcher manifest digest is shown on DockerHub and in the reproducible build script output. To verify it, build the launcher image yourself from the same commit and compare the manifest digest. * Download the MPC code from this repository: @@ -1661,7 +1663,8 @@ cd mpc/ git checkout ``` -* Compile it using the reproducible build script. For this you need to install `repro-env`, `docker-buildx`, and `skopeo`, and have the `docker` daemon running. +* Build it using the reproducible build script. You need `nix` (with flakes + enabled) installed locally; no docker daemon required. ```bash $ ./deployment/build-images.sh --rust-launcher @@ -1670,6 +1673,9 @@ rust launcher binary hash: rust launcher manifest digest: sha256: ``` + See [reproducible-builds.md](reproducible-builds.md) for the underlying Nix + derivations if you prefer to invoke them directly. + The `rust launcher manifest digest` is what you vote for. When submitting the `launcher_hash` value in the voting command, strip the `sha256:` prefix and provide only the hex digest. * Do your own due diligence on the code/binary. diff --git a/flake.nix b/flake.nix index ea6044ed58..a0f72d0046 100644 --- a/flake.nix +++ b/flake.nix @@ -51,22 +51,84 @@ # PCLMUL-accelerated CRC32C path is compiled in. Production node # fleet is all v3-capable (Haswell / Excavator and newer). # - # Shared between the reproducible mpc-node build (nix/mpc-node.nix) - # and the dev shell (devShells.default below) so feature-test macros + # Shared between the reproducible mpc-node / tee-launcher builds + # (nix/mpc-node.nix) and the dev shell (devShells.default below) so + # feature-test macros # in bindgen-parsed headers, cc-rs-compiled C/C++ deps, and the # rustc target-cpu line up across all build paths. prodCFlags = "-march=x86-64-v3 -mpclmul -maes"; in { - packages = forAllSystems (pkgs: { - mpc-node = pkgs.callPackage ./nix/mpc-node.nix { - inherit crane prodCFlags; - }; - mpc-contract = pkgs.callPackage ./nix/mpc-contract.nix { - cargo-near = pkgs.callPackage ./nix/cargo-near.nix { }; - }; - }); + packages = forAllSystems ( + pkgs: + let + # Shared Rust-binary builder. Each leaf in nix/.nix calls + # this with its crate-specific buildInputs / pname / cargoExtraArgs; + # see nix/rust-build-common.nix for what's shared. + buildRustBin = pkgs.callPackage ./nix/rust-build-common.nix { + inherit crane prodCFlags; + }; + + mpc-node = pkgs.callPackage ./nix/mpc-node.nix { + inherit buildRustBin; + # The Nix sandbox strips `.git`, so the `built` crate can't read + # the commit at compile time. Hand it the flake's own revision so + # `mpc-node --version` / the build-info metrics keep reporting it. + # Falls back to "unknown" for dirty trees without a usable rev. + gitCommitHashShort = self.shortRev or self.dirtyShortRev or "unknown"; + }; + tee-launcher = pkgs.callPackage ./nix/tee-launcher.nix { inherit buildRustBin; }; + + mkImageDir = image: pkgs.callPackage ./nix/image-dir.nix { inherit image; }; + mkManifestDigest = + imageDir: pkgs.callPackage ./nix/image-manifest-digest.nix { inherit imageDir; }; + in + { + inherit mpc-node tee-launcher; + + # Registry copy/push tool, re-exported so `nix run .#skopeo` works + # without any system-installed tooling. Pinned through flake.lock, + # so it's the exact same skopeo that image-dir.nix used to produce + # the layouts being pushed. + inherit (pkgs) skopeo; + + mpc-contract = pkgs.callPackage ./nix/mpc-contract.nix { + cargo-near = pkgs.callPackage ./nix/cargo-near.nix { }; + }; + } + # `dockerTools.buildLayeredImage` produces a Linux container image, + # so only expose the image / digest derivations on Linux builders. + // lib.optionalAttrs pkgs.stdenv.isLinux ( + let + node-image = pkgs.callPackage ./nix/node-image.nix { inherit mpc-node; }; + node-gcp-image = pkgs.callPackage ./nix/node-gcp-image.nix { inherit mpc-node; }; + rust-launcher-image = pkgs.callPackage ./nix/rust-launcher-image.nix { + inherit tee-launcher; + }; + + # `dir:` layouts with compressed blobs — the artifacts that get + # pushed; the manifest digests are hashes of these exact bytes. + node-image-dir = mkImageDir node-image; + node-gcp-image-dir = mkImageDir node-gcp-image; + rust-launcher-image-dir = mkImageDir rust-launcher-image; + in + { + inherit + node-image + node-gcp-image + rust-launcher-image + node-image-dir + node-gcp-image-dir + rust-launcher-image-dir + ; + + node-image-manifest-digest = mkManifestDigest node-image-dir; + node-gcp-image-manifest-digest = mkManifestDigest node-gcp-image-dir; + rust-launcher-image-manifest-digest = mkManifestDigest rust-launcher-image-dir; + } + ) + ); devShells = forAllSystems ( pkgs: diff --git a/nix/image-dir.nix b/nix/image-dir.nix new file mode 100644 index 0000000000..fb394f2867 --- /dev/null +++ b/nix/image-dir.nix @@ -0,0 +1,45 @@ +{ + runCommand, + skopeo, + image, +}: + +# Convert a `dockerTools.buildLayeredImage` docker-archive tarball into +# skopeo's `dir:` layout — manifest.json plus gzip-compressed layer blobs — +# by running skopeo entirely inside the Nix sandbox. Both source +# (`docker-archive:`) and destination (`dir:`) are local formats, so no +# network is touched; the skopeo doing the compression is the one pinned by +# flake.lock, making the output bit-identical across builders. +# +# This is the artifact that gets pushed to the registry. `skopeo copy +# --preserve-digests dir: docker://...` uploads these exact blobs and +# manifest, so the digest the registry reports is by construction +# `sha256(manifest.json)` — it cannot drift with whatever skopeo version +# happens to be installed on the pushing machine. + +runCommand "${image.imageName}-image-dir" + { + nativeBuildInputs = [ skopeo ]; + } + '' + workdir=$(mktemp -d) + mkdir "$workdir/skopeo-tmp" + + # Two flags to make skopeo work in the Nix sandbox: + # + # `--insecure-policy` skips the signature-trust policy check. Inside the + # sandbox $HOME is `/homeless-shelter` and /etc/containers/policy.json + # doesn't exist, so the default policy lookup fails. The check is also + # meaningless here: we're converting a local docker-archive tarball to a + # local dir layout, no registry signatures involved. + # + # `--tmpdir` overrides containers/image's hardcoded /var/tmp big-files + # temp dir. The Nix sandbox root is read-only so we can't create + # /var/tmp; point skopeo at our writable workdir instead. + skopeo \ + --insecure-policy \ + --tmpdir "$workdir/skopeo-tmp" \ + copy --all --dest-compress \ + "docker-archive:${image}" \ + "dir:$out" + '' diff --git a/nix/image-manifest-digest.nix b/nix/image-manifest-digest.nix new file mode 100644 index 0000000000..573124025f --- /dev/null +++ b/nix/image-manifest-digest.nix @@ -0,0 +1,15 @@ +{ + runCommand, + imageDir, +}: + +# The registry manifest digest of an image — the value operators vote for. +# +# `imageDir` (see image-dir.nix) holds the exact manifest and compressed +# blobs that get pushed, so hashing its manifest.json yields — by +# construction, not by coincidence — the digest the registry will report +# after `skopeo copy --preserve-digests dir: docker://...`. + +runCommand "${imageDir.name}-manifest-digest" { } '' + printf 'sha256:%s\n' "$(sha256sum < ${imageDir}/manifest.json | cut -d' ' -f1)" > $out +'' diff --git a/nix/mpc-node.nix b/nix/mpc-node.nix index b3d15d6d2b..2e2a3a4fc8 100644 --- a/nix/mpc-node.nix +++ b/nix/mpc-node.nix @@ -1,11 +1,6 @@ { lib, - pkgs, stdenv, - rustPlatform, - rust-bin, - llvmPackages_19, - pkg-config, openssl, zlib, libiconv, @@ -15,279 +10,42 @@ bzip2, udev, dbus, - apple-sdk_14 ? null, - crane, - # Shared production ISA flag string (e.g. "-march=x86-64-v3 -mpclmul -maes"), - # passed in from flake.nix so the dev shell, the reproducible build, and - # bindgen-parsed headers all agree on the same feature-test macros. - prodCFlags, + buildRustBin, + # Short git revision injected from flake.nix (`self.shortRev`); the build + # sandbox has no `.git` for the `built` crate to probe. + gitCommitHashShort, }: -let - llvmPkgs = llvmPackages_19; +# mpc-node links nearcore's rocksdb (snappy/lz4/zstd/bzip2), openssl, and +# udev/dbus on Linux. Everything not listed here flows from the shared +# builder in nix/rust-build-common.nix. - # Pin the Rust toolchain to rust-toolchain.toml so Nix builds match what - # `cargo build` uses locally. - rustToolchain = (rust-bin.fromRustupToolchainFile ../rust-toolchain.toml).override { - extensions = [ "rust-src" ]; - }; - - craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; - - isX86 = stdenv.hostPlatform.isx86_64; - - # `prodCFlags` (passed from flake.nix) carries the production ISA string — - # see flake.nix for the rationale. Pinned to a fixed level rather than - # `-march=native` so output bytes don't vary with the build host's CPU. - marchFlag = lib.optionalString isX86 prodCFlags; - - # Take the version from [workspace.package.version] so this file stays in - # sync on every release bump. - workspaceCargoToml = lib.importTOML ../Cargo.toml; +buildRustBin { pname = "mpc-node"; - version = workspaceCargoToml.workspace.package.version; - - # Source filter. `filterCargoSources` keeps `.rs`, `Cargo.toml`, `Cargo.lock` - # and a couple of other Cargo-relevant files — anything else that is pulled - # in by `include_str!` / `include_bytes!` at compile time must be allow- - # listed here, otherwise the build fails with "file not found". - src = lib.cleanSourceWith { - src = craneLib.path ../.; - filter = - path: type: - (craneLib.filterCargoSources path type) - # `#![doc = include_str!("../README.md")]` on these two crates: - || (lib.hasSuffix "crates/contract/README.md" path) - || (lib.hasSuffix "crates/near-mpc-contract-interface/README.md" path) - # `include_str!` for the rendered third-party license bundle served by - # the node's web handler (crates/node/src/web.rs). - || (lib.hasSuffix "third-party-licenses/licenses.html" path) - # `include_bytes!` for historical signer contract snapshots - # (crates/contract-history/src/lib.rs). - || (lib.hasInfix "/crates/contract-history/archive/" path) - # Every `crates/*/assets/` directory — picked up by various - # `include_str!` / `include_bytes!` calls across the workspace. - || (lib.hasInfix "/assets/" path); - }; - - # Vendor the cargo lockfile via nixpkgs' importCargoLock instead of crane's - # default `cargo package`-based vendoring. Reason: `cargo package` only ships - # files inside a crate's own directory. Some git deps (e.g. nearcore's - # `near-jsonrpc`) pull files from sibling directories via - # `include_bytes!("../../../...")`; those get stripped by cargo's packaging - # rules. `fetchgit` copies the entire git checkout, preserving siblings. - # - # `allowBuiltinFetchGit = true` uses `builtins.fetchGit`, which is - # reproducible: the revision fully determines content, no `sha256` needed. - importedVendorDir = rustPlatform.importCargoLock { - lockFile = ../Cargo.lock; - allowBuiltinFetchGit = true; + cargoExtraArgs = "-p mpc-node --bin mpc-node --locked"; + description = "MPC node binary for NEAR threshold signer"; + + buildInputs = [ + openssl + zlib + libiconv + snappy + lz4 + zstd + bzip2 + ] + ++ lib.optionals stdenv.isLinux [ + udev + dbus + ]; + + extraEnv = { + # The `built` crate reports GIT_COMMIT_HASH_SHORT = None inside the Nix + # sandbox (no `.git`), which would surface as "unknown" in + # `mpc-node --version` and the build-info metrics. Override it with the + # flake's revision. Same idea as the BUILT_OVERRIDE used by the old + # repro-env pipeline (#3441), and equally reproducible: the value is a + # pure function of the commit being built. + BUILT_OVERRIDE_mpc_node_GIT_COMMIT_HASH_SHORT = gitCommitHashShort; }; - - # Repackage `importedVendorDir` into the layout crane expects, with one - # extra wrinkle: - # - # `near-jsonrpc` (lib.rs) does `include_bytes!("../../../chain/jsonrpc/ - # openapi/openapi.json")` — a workspace-relative path that only resolves - # in the original nearcore checkout, where the crate lives at - # `chain/jsonrpc/`. Cargo's vendor format flattens every crate to a - # single directory, so the macro's `..` traversal walks past the vendor - # root and the file is "not found". - # - # We work around this by laying out the vendor dir so that the `..` - # traversal lands somewhere we control: - # - # $out/ ← cargoVendorDir (3 ups from src/lib.rs) - # ├── vendor/ ← config.toml's `directory = "..."` - # │ ├── near-jsonrpc-X.Y.Z ──┐ relative symlink (stays inside $out) - # │ └── (other crates as symlinks into importedVendorDir) - # ├── chain/jsonrpc/ ←──────────┘ real copy of the near-jsonrpc tree - # │ ├── src/lib.rs - # │ └── openapi/openapi.json ← what the broken macro wants - # └── config.toml ← crane reads this - # - # The relative `../chain/jsonrpc` symlink keeps `..` traversal inside - # `$out` (an absolute symlink to importedVendorDir would push it back - # into /nix/store and re-break path resolution). The crate has to be a - # real directory tree (not symlinks) for `..` to walk through it - # correctly, hence `cp -aL`. - cargoVendorDir = pkgs.runCommand "vendor-cargo-deps-crane" { } '' - mkdir -p $out/vendor $out/chain - - # Match only `near-jsonrpc-` - set -- ${importedVendorDir}/near-jsonrpc-[0-9]* - if [ "$1" = '${importedVendorDir}/near-jsonrpc-[0-9]*' ]; then - echo "error: could not find vendored near-jsonrpc crate in ${importedVendorDir}" >&2 - exit 1 - fi - if [ "$#" -ne 1 ]; then - echo "error: expected exactly one vendored near-jsonrpc crate in ${importedVendorDir}, found $# matches" >&2 - exit 1 - fi - near_jsonrpc_entry="$1" - near_jsonrpc_name=$(basename "$near_jsonrpc_entry") - - cp -aL "$near_jsonrpc_entry" $out/chain/jsonrpc - chmod -R u+w $out/chain/jsonrpc - ln -s ../chain/jsonrpc "$out/vendor/$near_jsonrpc_name" - - for entry in ${importedVendorDir}/*; do - name=$(basename "$entry") - if [ "$name" != "$near_jsonrpc_name" ]; then - ln -s "$entry" "$out/vendor/" - fi - done - - sed "s|directory = \"cargo-vendor-dir\"|directory = \"$out/vendor\"|" \ - ${importedVendorDir}/.cargo/config.toml > $out/config.toml - ''; - - commonArgs = { - inherit - pname - version - src - cargoVendorDir - ; - - strictDeps = true; - cargoProfile = "reproducible"; - cargoExtraArgs = "-p mpc-node --bin mpc-node --locked"; - - nativeBuildInputs = [ - pkg-config - # Sets LIBCLANG_PATH and the base BINDGEN_EXTRA_CLANG_ARGS (clang - # resource dir, libc headers) so rust-bindgen works inside the Nix - # sandbox. Anything we add to BINDGEN_EXTRA_CLANG_ARGS below is appended - # to the hook's value, not replaced. - rustPlatform.bindgenHook - llvmPkgs.clang - ]; - - buildInputs = [ - openssl - zlib - libiconv - snappy - lz4 - zstd - bzip2 - ] - ++ lib.optionals stdenv.isLinux [ - udev - dbus - ] - ++ lib.optionals stdenv.isDarwin [ - # Modern apple-sdk_14 bundles Security / SystemConfiguration / - # CoreFoundation and friends; no need to list them separately. - apple-sdk_14 - ]; - - env = { - # Reproducibility knobs ------------------------------------------------ - - # Fixed epoch for any build-script that stamps timestamps into output. - SOURCE_DATE_EPOCH = "0"; - - # Link against nixpkgs' openssl instead of openssl-sys' vendored copy. - # The vendored copy varies with the host; the nixpkgs one is pinned - # through flake.lock. - OPENSSL_NO_VENDOR = "1"; - - # Prevents rocksdb's build.rs from probing /proc/cpuinfo and baking - # host-specific ISA choices into its object files. - PORTABLE = "1"; - - # tikv-jemalloc-sys runs jemalloc's `./configure`, which auto-detects - # these from the build host (CPUID, sysconf, /proc/meminfo). When the - # detected values diverge between builders, the static `emap_global` - # rtree is sized differently and .bss — plus a few inlined .text - # constants — drift. Pin to the standard x86_64 Linux values; values - # are base-2 logarithms (so LG_PAGE=12 ↔ 2^12 B = 4 KiB). Option - # semantics: - # https://github.com/jemalloc/jemalloc/blob/5.3.0/INSTALL.md#advanced-configuration - # - # 48-bit user VA (4-level paging) - # https://github.com/torvalds/linux/blob/v6.7/Documentation/arch/x86/x86_64/mm.rst#L7 - JEMALLOC_SYS_WITH_LG_VADDR = "48"; - # 4 KiB base page (PAGE_SHIFT = 12) - # https://github.com/torvalds/linux/blob/v6.7/arch/x86/include/asm/page_types.h#L10 - JEMALLOC_SYS_WITH_LG_PAGE = "12"; - # 2 MiB huge page (HPAGE_SHIFT = PMD_SHIFT = 21) - # https://github.com/torvalds/linux/blob/v6.7/arch/x86/include/asm/pgtable_64_types.h#L91 - JEMALLOC_SYS_WITH_LG_HUGEPAGE = "21"; - - # Pin the target ISA for both C/C++ (cc-crate for rocksdb, snappy, - # zstd, ...) and Rust itself. Without this, the cc crate defaults to - # the build host's CPU and output bytes vary by machine. - CFLAGS = marchFlag; - CXXFLAGS = "-include cstdint ${marchFlag}"; - - RUSTFLAGS = lib.concatStringsSep " " ( - lib.optionals isX86 [ "-C target-cpu=x86-64-v3" ] - ++ [ - # Scrub the vendor dir's /nix/store path out of rustc-emitted debug - # info and panic messages. The build-sandbox path is handled in - # `preBuild` below; do NOT add `${src}` here — it would re-key - # cargoArtifacts on every source change and defeat the dep cache. - "--remap-path-prefix=${cargoVendorDir}=/cargo-vendor" - ] - ); - - # Extra bindgen flags — paths are already provided by bindgenHook. - BINDGEN_EXTRA_CLANG_ARGS = marchFlag; - } - // lib.optionalAttrs stdenv.isDarwin { - # Deployment target is independent of the SDK version; pin it so the - # Mach-O LC_BUILD_VERSION load command is identical across builders. - MACOSX_DEPLOYMENT_TARGET = "14.0"; - }; - - # Remap the runtime build directory. - # - # The `${src}` remap in RUSTFLAGS above only rewrites - # `/nix/store/-source` paths, but rustc never sees those at - # compile time — it sees the path where Nix actually unpacks the - # sources, i.e. `$NIX_BUILD_TOP/source/...`. That path varies by Nix - # installation type: - # - # * multi-user (daemon) Nix on Linux: `$NIX_BUILD_TOP` is `/build`, - # because the daemon bind-mounts the sandbox there. - # * single-user (per-user) Nix on Linux: no daemon, so no `/build` - # mount — `$NIX_BUILD_TOP` is `/nix/var/nix/builds/nix--`. - # * Nix on macOS: same per-build temp dir as single-user Linux, - # `/nix/var/nix/builds/nix--`. - # - # Without this remap, only multi-user Linux builds happen to be - # reproducible (and only because `/build` coincides with the remap - # target above); per-user Linux and any macOS build embeds its own - # ephemeral sandbox path in panic messages, debug info, and - # `track_caller` location strings, which makes the binary differ from - # builds on other machines. This hook normalises `$NIX_BUILD_TOP` to - # `/build` for everyone so the output is bit-identical regardless of - # how Nix is installed on the builder. - preBuild = '' - export RUSTFLAGS="$RUSTFLAGS --remap-path-prefix=$NIX_BUILD_TOP/source=/build/source --remap-path-prefix=$NIX_BUILD_TOP=/build" - ''; - - doCheck = false; - }; - - # Build deps in a separate derivation so that they're cached across - # mpc-node source changes. - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - -in -craneLib.buildPackage ( - commonArgs - // { - inherit cargoArtifacts; - - meta = with lib; { - description = "MPC node binary for NEAR threshold signer"; - license = licenses.mit; - platforms = platforms.unix; - mainProgram = "mpc-node"; - }; - } -) +} diff --git a/nix/node-gcp-image.nix b/nix/node-gcp-image.nix new file mode 100644 index 0000000000..7ed3fa1a84 --- /dev/null +++ b/nix/node-gcp-image.nix @@ -0,0 +1,49 @@ +{ + dockerTools, + bash, + coreutils, + gnused, + python3, + cacert, + google-cloud-sdk, + runCommand, + mpc-node, +}: + +# GCP variant of the node image — same start.sh / genesis layout as the +# vanilla node image, plus `gcloud` so start.sh can resolve the +# `gcloud secrets versions access latest ...` calls used to populate +# MPC_P2P_PRIVATE_KEY / MPC_ACCOUNT_SK / MPC_SECRET_STORE_KEY at boot. + +let + appFiles = runCommand "mpc-node-gcp-app-files" { } '' + mkdir -p $out/app + install -m 0755 ${../deployment/start.sh} $out/app/start.sh + install -m 0644 ${../deployment/localnet/genesis.json} $out/app/localnet-genesis.json + ln -s ${mpc-node}/bin/mpc-node $out/app/mpc-node + ''; +in +dockerTools.buildLayeredImage { + name = "mpc-node-gcp"; + tag = "latest"; + created = "1970-01-01T00:00:00Z"; + + contents = [ + bash + coreutils + gnused + python3 + cacert + google-cloud-sdk + appFiles + ]; + + config = { + Cmd = [ "/app/start.sh" ]; + WorkingDir = "/app"; + Env = [ + "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "PATH=/bin:/usr/bin" + ]; + }; +} diff --git a/nix/node-image.nix b/nix/node-image.nix new file mode 100644 index 0000000000..5b80294aa9 --- /dev/null +++ b/nix/node-image.nix @@ -0,0 +1,54 @@ +{ + dockerTools, + bash, + coreutils, + gnused, + python3, + cacert, + runCommand, + mpc-node, +}: + +let + # The runtime layout `start.sh` expects: a writable-looking `/app` directory + # holding the entry script, the embedded localnet genesis, and the binary + # (referenced as both `./mpc-node` and `/app/mpc-node`). + appFiles = runCommand "mpc-node-app-files" { } '' + mkdir -p $out/app + install -m 0755 ${../deployment/start.sh} $out/app/start.sh + install -m 0644 ${../deployment/localnet/genesis.json} $out/app/localnet-genesis.json + ln -s ${mpc-node}/bin/mpc-node $out/app/mpc-node + ''; +in +dockerTools.buildLayeredImage { + name = "mpc-node"; + tag = "latest"; + # Fixed creation time for repro; image manifest digests must not depend on + # wall clock at build time. + created = "1970-01-01T00:00:00Z"; + + # `bash` provides /bin/bash for the start.sh shebang. + # `coreutils` and `gnused` provide the `cp`, `rm`, `sed -i` calls in start.sh. + # `python3` is used by start.sh for JSON manipulation. + # `cacert` provides the CA bundle for SSL_CERT_FILE. + # `mpc-node` is included transitively via the symlink in `appFiles`; the + # closure scanner picks up its store path and dockerTools pulls in the full + # runtime closure (libssl, glibc, ...). + contents = [ + bash + coreutils + gnused + python3 + cacert + appFiles + ]; + + config = { + Cmd = [ "/app/start.sh" ]; + WorkingDir = "/app"; + Env = [ + "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "PATH=/bin:/usr/bin" + ]; + }; +} diff --git a/nix/rust-build-common.nix b/nix/rust-build-common.nix new file mode 100644 index 0000000000..aafc9f3982 --- /dev/null +++ b/nix/rust-build-common.nix @@ -0,0 +1,297 @@ +{ + lib, + pkgs, + stdenv, + rustPlatform, + rust-bin, + llvmPackages_19, + pkg-config, + apple-sdk_14 ? null, + crane, + # Shared production ISA flag string (e.g. "-march=x86-64-v3 -mpclmul -maes"), + # passed in from flake.nix so the dev shell, the reproducible build, and + # bindgen-parsed headers all agree on the same feature-test macros. + prodCFlags, +}: + +# Returns a builder function. Each per-crate leaf (nix/mpc-node.nix, +# nix/tee-launcher.nix) calls this with its `pname` / `cargoExtraArgs` / +# crate-specific `buildInputs`, and gets back a fully-configured +# craneLib.buildPackage derivation. +# +# Everything in here is shared across all reproducible binaries: the source +# filter, the cargo vendor dir layout (with the near-jsonrpc workaround), +# the build env (CFLAGS, JEMALLOC_*, RUSTFLAGS), and the path-remap preBuild +# hook. Anything binary-specific (link-time C deps, metadata) flows in via +# the function arguments below. + +{ + pname, + cargoExtraArgs, + buildInputs ? [ ], + description ? "Rust binary built from the mpc workspace", + mainProgram ? pname, + # Extra env vars applied ONLY to the final crate build, not to the + # buildDepsOnly derivation. Anything that changes on every commit (e.g. + # a git-revision override) belongs here — putting it in `commonArgs.env` + # would re-key cargoArtifacts each commit and defeat the dep cache. + extraEnv ? { }, +}: + +let + llvmPkgs = llvmPackages_19; + + # Pin the Rust toolchain to rust-toolchain.toml so Nix builds match what + # `cargo build` uses locally. + rustToolchain = (rust-bin.fromRustupToolchainFile ../rust-toolchain.toml).override { + extensions = [ "rust-src" ]; + }; + + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + isX86 = stdenv.hostPlatform.isx86_64; + + # `prodCFlags` (passed from flake.nix) carries the production ISA string — + # see flake.nix for the rationale. Pinned to a fixed level rather than + # `-march=native` so output bytes don't vary with the build host's CPU. + marchFlag = lib.optionalString isX86 prodCFlags; + + # Take the version from [workspace.package.version] so this file stays in + # sync on every release bump. + workspaceCargoToml = lib.importTOML ../Cargo.toml; + version = workspaceCargoToml.workspace.package.version; + + # Source filter. `filterCargoSources` keeps `.rs`, `Cargo.toml`, `Cargo.lock` + # and a couple of other Cargo-relevant files — anything else that is pulled + # in by `include_str!` / `include_bytes!` at compile time must be allow- + # listed here, otherwise the build fails with "file not found". + src = lib.cleanSourceWith { + src = craneLib.path ../.; + filter = + path: type: + (craneLib.filterCargoSources path type) + # `#![doc = include_str!("../README.md")]` on these two crates: + || (lib.hasSuffix "crates/contract/README.md" path) + || (lib.hasSuffix "crates/near-mpc-contract-interface/README.md" path) + # `include_str!` for the rendered third-party license bundle served by + # the node's web handler (crates/node/src/web.rs). + || (lib.hasSuffix "third-party-licenses/licenses.html" path) + # `include_bytes!` for historical signer contract snapshots + # (crates/contract-history/src/lib.rs). + || (lib.hasInfix "/crates/contract-history/archive/" path) + # Every `crates/*/assets/` directory — picked up by various + # `include_str!` / `include_bytes!` calls across the workspace. + || (lib.hasInfix "/assets/" path); + }; + + # Vendor the cargo lockfile via nixpkgs' importCargoLock instead of crane's + # default `cargo package`-based vendoring. Reason: `cargo package` only ships + # files inside a crate's own directory. Some git deps (e.g. nearcore's + # `near-jsonrpc`) pull files from sibling directories via + # `include_bytes!("../../../...")`; those get stripped by cargo's packaging + # rules. `fetchgit` copies the entire git checkout, preserving siblings. + # + # `allowBuiltinFetchGit = true` uses `builtins.fetchGit`, which is + # reproducible: the revision fully determines content, no `sha256` needed. + importedVendorDir = rustPlatform.importCargoLock { + lockFile = ../Cargo.lock; + allowBuiltinFetchGit = true; + }; + + # Repackage `importedVendorDir` into the layout crane expects, with one + # extra wrinkle: + # + # `near-jsonrpc` (lib.rs) does `include_bytes!("../../../chain/jsonrpc/ + # openapi/openapi.json")` — a workspace-relative path that only resolves + # in the original nearcore checkout, where the crate lives at + # `chain/jsonrpc/`. Cargo's vendor format flattens every crate to a + # single directory, so the macro's `..` traversal walks past the vendor + # root and the file is "not found". + # + # We work around this by laying out the vendor dir so that the `..` + # traversal lands somewhere we control: + # + # $out/ ← cargoVendorDir (3 ups from src/lib.rs) + # ├── vendor/ ← config.toml's `directory = "..."` + # │ ├── near-jsonrpc-X.Y.Z ──┐ relative symlink (stays inside $out) + # │ └── (other crates as symlinks into importedVendorDir) + # ├── chain/jsonrpc/ ←──────────┘ real copy of the near-jsonrpc tree + # │ ├── src/lib.rs + # │ └── openapi/openapi.json ← what the broken macro wants + # └── config.toml ← crane reads this + # + # The relative `../chain/jsonrpc` symlink keeps `..` traversal inside + # `$out` (an absolute symlink to importedVendorDir would push it back + # into /nix/store and re-break path resolution). The crate has to be a + # real directory tree (not symlinks) for `..` to walk through it + # correctly, hence `cp -aL`. + cargoVendorDir = pkgs.runCommand "vendor-cargo-deps-crane" { } '' + mkdir -p $out/vendor $out/chain + + # Match only `near-jsonrpc-` + set -- ${importedVendorDir}/near-jsonrpc-[0-9]* + if [ "$1" = '${importedVendorDir}/near-jsonrpc-[0-9]*' ]; then + echo "error: could not find vendored near-jsonrpc crate in ${importedVendorDir}" >&2 + exit 1 + fi + if [ "$#" -ne 1 ]; then + echo "error: expected exactly one vendored near-jsonrpc crate in ${importedVendorDir}, found $# matches" >&2 + exit 1 + fi + near_jsonrpc_entry="$1" + near_jsonrpc_name=$(basename "$near_jsonrpc_entry") + + cp -aL "$near_jsonrpc_entry" $out/chain/jsonrpc + chmod -R u+w $out/chain/jsonrpc + ln -s ../chain/jsonrpc "$out/vendor/$near_jsonrpc_name" + + for entry in ${importedVendorDir}/*; do + name=$(basename "$entry") + if [ "$name" != "$near_jsonrpc_name" ]; then + ln -s "$entry" "$out/vendor/" + fi + done + + sed "s|directory = \"cargo-vendor-dir\"|directory = \"$out/vendor\"|" \ + ${importedVendorDir}/.cargo/config.toml > $out/config.toml + ''; + + commonArgs = { + inherit + pname + version + src + cargoVendorDir + cargoExtraArgs + ; + + strictDeps = true; + cargoProfile = "reproducible"; + + nativeBuildInputs = [ + pkg-config + # Sets LIBCLANG_PATH and the base BINDGEN_EXTRA_CLANG_ARGS (clang + # resource dir, libc headers) so rust-bindgen works inside the Nix + # sandbox. Anything we add to BINDGEN_EXTRA_CLANG_ARGS below is appended + # to the hook's value, not replaced. + rustPlatform.bindgenHook + llvmPkgs.clang + ]; + + # Caller supplies crate-specific link-time deps. Modern apple-sdk_14 + # bundles Security / SystemConfiguration / CoreFoundation and friends, + # which any darwin Rust build needs at link time, so we add it here + # uniformly rather than making every leaf repeat the conditional. + buildInputs = buildInputs ++ lib.optionals stdenv.isDarwin [ apple-sdk_14 ]; + + env = { + # Reproducibility knobs ------------------------------------------------ + + # Fixed epoch for any build-script that stamps timestamps into output. + SOURCE_DATE_EPOCH = "0"; + + # Link against nixpkgs' openssl instead of openssl-sys' vendored copy. + # The vendored copy varies with the host; the nixpkgs one is pinned + # through flake.lock. (No-op for crates that don't link openssl.) + OPENSSL_NO_VENDOR = "1"; + + # Prevents rocksdb's build.rs from probing /proc/cpuinfo and baking + # host-specific ISA choices into its object files. (No-op for crates + # that don't link rocksdb.) + PORTABLE = "1"; + + # tikv-jemalloc-sys runs jemalloc's `./configure`, which auto-detects + # these from the build host (CPUID, sysconf, /proc/meminfo). When the + # detected values diverge between builders, the static `emap_global` + # rtree is sized differently and .bss — plus a few inlined .text + # constants — drift. Pin to the standard x86_64 Linux values; values + # are base-2 logarithms (so LG_PAGE=12 ↔ 2^12 B = 4 KiB). Option + # semantics: + # https://github.com/jemalloc/jemalloc/blob/5.3.0/INSTALL.md#advanced-configuration + # (No-op for crates that don't link jemalloc.) + # + # 48-bit user VA (4-level paging) + # https://github.com/torvalds/linux/blob/v6.7/Documentation/arch/x86/x86_64/mm.rst#L7 + JEMALLOC_SYS_WITH_LG_VADDR = "48"; + # 4 KiB base page (PAGE_SHIFT = 12) + # https://github.com/torvalds/linux/blob/v6.7/arch/x86/include/asm/page_types.h#L10 + JEMALLOC_SYS_WITH_LG_PAGE = "12"; + # 2 MiB huge page (HPAGE_SHIFT = PMD_SHIFT = 21) + # https://github.com/torvalds/linux/blob/v6.7/arch/x86/include/asm/pgtable_64_types.h#L91 + JEMALLOC_SYS_WITH_LG_HUGEPAGE = "21"; + + # Pin the target ISA for both C/C++ (cc-crate for rocksdb, snappy, + # zstd, ...) and Rust itself. Without this, the cc crate defaults to + # the build host's CPU and output bytes vary by machine. + CFLAGS = marchFlag; + CXXFLAGS = "-include cstdint ${marchFlag}"; + + RUSTFLAGS = lib.concatStringsSep " " ( + lib.optionals isX86 [ "-C target-cpu=x86-64-v3" ] + ++ [ + # Scrub the vendor dir's /nix/store path out of rustc-emitted debug + # info and panic messages. The build-sandbox path is handled in + # `preBuild` below; do NOT add `${src}` here — it would re-key + # cargoArtifacts on every source change and defeat the dep cache. + "--remap-path-prefix=${cargoVendorDir}=/cargo-vendor" + ] + ); + + # Extra bindgen flags — paths are already provided by bindgenHook. + BINDGEN_EXTRA_CLANG_ARGS = marchFlag; + } + // lib.optionalAttrs stdenv.isDarwin { + # Deployment target is independent of the SDK version; pin it so the + # Mach-O LC_BUILD_VERSION load command is identical across builders. + MACOSX_DEPLOYMENT_TARGET = "14.0"; + }; + + # Remap the runtime build directory. + # + # The `${src}` remap in RUSTFLAGS above only rewrites + # `/nix/store/-source` paths, but rustc never sees those at + # compile time — it sees the path where Nix actually unpacks the + # sources, i.e. `$NIX_BUILD_TOP/source/...`. That path varies by Nix + # installation type: + # + # * multi-user (daemon) Nix on Linux: `$NIX_BUILD_TOP` is `/build`, + # because the daemon bind-mounts the sandbox there. + # * single-user (per-user) Nix on Linux: no daemon, so no `/build` + # mount — `$NIX_BUILD_TOP` is `/nix/var/nix/builds/nix--`. + # * Nix on macOS: same per-build temp dir as single-user Linux, + # `/nix/var/nix/builds/nix--`. + # + # Without this remap, only multi-user Linux builds happen to be + # reproducible (and only because `/build` coincides with the remap + # target above); per-user Linux and any macOS build embeds its own + # ephemeral sandbox path in panic messages, debug info, and + # `track_caller` location strings, which makes the binary differ from + # builds on other machines. This hook normalises `$NIX_BUILD_TOP` to + # `/build` for everyone so the output is bit-identical regardless of + # how Nix is installed on the builder. + preBuild = '' + export RUSTFLAGS="$RUSTFLAGS --remap-path-prefix=$NIX_BUILD_TOP/source=/build/source --remap-path-prefix=$NIX_BUILD_TOP=/build" + ''; + + doCheck = false; + }; + + # Build deps in a separate derivation so that they're cached across + # source changes. + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + +in +craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + + env = commonArgs.env // extraEnv; + + meta = { + inherit description mainProgram; + license = lib.licenses.mit; + platforms = lib.platforms.unix; + }; + } +) diff --git a/nix/rust-launcher-image.nix b/nix/rust-launcher-image.nix new file mode 100644 index 0000000000..1ff7e51fae --- /dev/null +++ b/nix/rust-launcher-image.nix @@ -0,0 +1,42 @@ +{ + dockerTools, + cacert, + docker-client, + runCommand, + tee-launcher, +}: + +# Image for the TEE launcher (`tee-launcher`). The launcher shells out to +# `docker compose -f ... up -d` and `docker pull`, so the image needs the +# docker CLI plus the v2 compose plugin. nixpkgs' `docker-client` is a +# CLI-only build of docker that ships the compose plugin. +# +# `/app-data` and `/mnt/shared` are pre-created here so they exist at runtime +# without depending on the launcher to mkdir on first boot. + +let + appFiles = runCommand "tee-launcher-app-files" { } '' + mkdir -p $out/app $out/app-data $out/mnt/shared + ln -s ${tee-launcher}/bin/tee-launcher $out/app/tee-launcher + ''; +in +dockerTools.buildLayeredImage { + name = "mpc-rust-launcher"; + tag = "latest"; + created = "1970-01-01T00:00:00Z"; + + contents = [ + cacert + docker-client + appFiles + ]; + + config = { + Cmd = [ "/app/tee-launcher" ]; + WorkingDir = "/app"; + Env = [ + "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "PATH=/bin:/usr/bin" + ]; + }; +} diff --git a/nix/tee-launcher.nix b/nix/tee-launcher.nix new file mode 100644 index 0000000000..9ecd816b91 --- /dev/null +++ b/nix/tee-launcher.nix @@ -0,0 +1,13 @@ +{ + buildRustBin, +}: + +# tee-launcher's deps are pure Rust (rustls for TLS, tokio, clap, serde, +# ...) — no link-time C libraries beyond what the shared builder already +# arranges for (libc, plus apple-sdk_14 on darwin). + +buildRustBin { + pname = "tee-launcher"; + cargoExtraArgs = "-p tee-launcher --bin tee-launcher --locked"; + description = "Launcher binary for the MPC TEE"; +} diff --git a/scripts/build-and-verify-rust-launcher-docker-image.sh b/scripts/build-and-verify-rust-launcher-docker-image.sh index 6afb858822..574221493d 100755 --- a/scripts/build-and-verify-rust-launcher-docker-image.sh +++ b/scripts/build-and-verify-rust-launcher-docker-image.sh @@ -7,11 +7,8 @@ set -euo pipefail -./deployment/build-images.sh --rust-launcher - -temp_dir=$(mktemp -d) -trap 'rm -rf "$temp_dir"' EXIT -echo "using $temp_dir" -skopeo copy --all --dest-compress docker-daemon:mpc-rust-launcher:latest dir:"$temp_dir" -built_hex="$(sha256sum "$temp_dir/manifest.json" | cut -d' ' -f1)" -echo "Built launcher image hash: sha256:${built_hex}" +# The `*-manifest-digest` derivation hashes the manifest of the pushed +# `dir:` layout (built by skopeo inside the Nix sandbox), so the digest is +# deterministic across builders and the only output is a `sha256:HEX` line. +nix build .#rust-launcher-image-manifest-digest --out-link result-rust-launcher-digest -L +echo "Built launcher image hash: $(cat result-rust-launcher-digest)"