diff --git a/.gitignore b/.gitignore index 1564d62f24..058a4d58ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .helix +build/ cobertura.xml target wallets/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..8605aead4c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,185 @@ +# syntax=docker/dockerfile:1 +# check=skip=UndefinedVar,UserExist + +# stages: +# - release: setup and builds release binaries +# - export: discrete stage for writing binaries into host build directory +# - runtime: prepares the release image +# +# We first set default values for build arguments used across the stages. +# Each stage must define the build arguments (ARGs) it uses. + +ARG FEATURES="" + +############################ +# Global build args +############################ +ARG UID=10901 +ARG GID=${UID} +ARG USER="user" +ARG HOME="/home/${USER}" +ARG CARGO_HOME="/usr/local/.cargo" +ARG CARGO_TARGET_DIR="${HOME}/target" +ARG TARGET_ARCH="x86_64-unknown-linux-musl" + +############################ +# Dependencies +############################ +# Build Deps +FROM stagex/pallet-rust:1.94.0@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 AS pallet-rust +FROM stagex/user-protobuf:26.1@sha256:a135aaf060990b6ef8a7c715c16f175811d3a1f5383970f5771adef05a0bc56a AS protobuf +FROM stagex/user-abseil-cpp:20240116.2@sha256:20a241145158a0aa7cb83ed5dc4f9ad6360dc975352787f4e6b00e8a39943f62 AS abseil-cpp +FROM stagex/core-sqlite3:3.50.1@sha256:8d2959fcde94119a724315d9c9f58acf59c5ae83cf4ad22a36ac1ed971327237 AS sqlite3 +# Runtime Deps +FROM stagex/core-busybox:1.37.0@sha256:d608daa946e4799cf28b105aba461db00187657bd55ea7c2935ff11dac237e27 AS busybox + + +############################ +# Release +############################ +FROM pallet-rust AS release +COPY --from=protobuf . / +COPY --from=abseil-cpp . / +COPY --from=sqlite3 . / + +SHELL ["/bin/sh", "-euo", "pipefail", "-c"] + +ARG HOME +WORKDIR ${HOME} + +ARG CARGO_INCREMENTAL +# default to 0, disables incremental compilation. +ENV CARGO_INCREMENTAL=${CARGO_INCREMENTAL:-0} + +ARG CARGO_HOME +ENV CARGO_HOME=${CARGO_HOME} + +ARG CARGO_TARGET_DIR +ARG TARGET_ARCH + +ARG FEATURES +ENV FEATURES=${FEATURES} + +ENV RUST_BACKTRACE=1 +ENV RUSTFLAGS="-C codegen-units=1" +ENV RUSTFLAGS="${RUSTFLAGS} -C target-feature=+crt-static" +ENV RUSTFLAGS="${RUSTFLAGS} -C link-arg=-Wl,--build-id=none" + +ENV SOURCE_DATE_EPOCH=1 + +# TODO : restore: +# cargo fetch --locked --target $TARGET_ARCH +# --locked was removed due to consistant breakage between Cargo.Toml and Cargo.lock +# see Github issues #2114 and #2311 + +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + --mount=type=cache,target=${CARGO_HOME}/git \ + --mount=type=bind,source=rust-toolchain.toml,target=rust-toolchain.toml,ro \ + --mount=type=bind,source=Cargo.toml,target=Cargo.toml,ro \ + --mount=type=bind,source=Cargo.lock,target=Cargo.lock,ro \ + --mount=type=bind,source=/darkside-tests,target=darkside-tests,ro \ + --mount=type=bind,source=/libtonode-tests,target=libtonode-tests,ro \ + --mount=type=bind,source=/pepper-sync,target=pepper-sync,ro \ + --mount=type=bind,source=/zingo-cli,target=zingo-cli,ro \ + --mount=type=bind,source=/zingolib,target=zingolib,ro \ + --mount=type=bind,source=/zingo-memo,target=zingo-memo,ro \ + --mount=type=bind,source=/zingo-price,target=zingo-price,ro \ + --mount=type=bind,source=/zingo-status,target=zingo-status,ro \ + --mount=type=bind,source=/zingolib_testutils,target=zingolib_testutils,ro \ + cargo fetch --target $TARGET_ARCH + +# TODO : --network=none was removed due to network requests in build script +# (docker level network denial) +# and cargo build requiring network access as well (see Github issue #2162) +# this needs to be re-added to ensure hermeticity +# +# TODO: additionally, restore +# cargo build --release --frozen --target $TARGET_ARCH --bin zingo-cli && install -D -m 0755 target/${TARGET_ARCH}/release/zingo-cli /usr/local/bin/zingo-cli +# --frozen was als removed due to build script + +# TODO : get rid of: +# --mount=type=cache,target=${HOME}/.zcash-params \ +# See Github issue #2314 +# this works! +# TODO : get rid of: +# --mount=type=cache,target=zingolib/zcash-params \ +# This soothes the savage beast as well! +# see Github issue #2315 + +RUN --mount=type=cache,target=${CARGO_HOME}/registry \ + --mount=type=cache,target=${CARGO_HOME}/git \ + --mount=type=cache,target=${HOME}/target \ + --mount=type=cache,target=${HOME}/.zcash-params \ + --mount=type=cache,target=zingolib/zcash-params \ + --mount=type=bind,source=rust-toolchain.toml,target=rust-toolchain.toml,ro \ + --mount=type=bind,source=Cargo.toml,target=Cargo.toml,ro \ + --mount=type=bind,source=Cargo.lock,target=Cargo.lock,ro \ + --mount=type=bind,source=/darkside-tests,target=darkside-tests,ro \ + --mount=type=bind,source=/libtonode-tests,target=libtonode-tests,ro \ + --mount=type=bind,source=/pepper-sync,target=pepper-sync,ro \ + --mount=type=bind,source=/zingo-cli,target=zingo-cli,ro \ + --mount=type=bind,source=/zingolib,target=zingolib,ro \ + --mount=type=bind,source=/zingo-memo,target=zingo-memo,ro \ + --mount=type=bind,source=/zingo-price,target=zingo-price,ro \ + --mount=type=bind,source=/zingo-status,target=zingo-status,ro \ + --mount=type=bind,source=/zingolib_testutils,target=zingolib_testutils,ro \ + cargo build --release ${FEATURES:+--features ${FEATURES}} --target $TARGET_ARCH --bin zingo-cli && install -D -m 0755 target/${TARGET_ARCH}/release/zingo-cli /usr/local/bin/zingo-cli + +############################ +# Export stage +############################ +FROM scratch AS export +COPY --from=release /usr/local/bin/zingo-cli /zingo-cli + +############################ +# Runtime stage +############################ +FROM busybox AS runtime + +# Create a non-privileged user for running `zingo-cli`. +# +# We use a high UID/GID (10901) to avoid overlap with host system users. +# This reduces the risk of container user namespace conflicts with host accounts, +# which could potentially lead to privilege escalation if a container escape occurs. +# +# We do not use the `--system` flag for user creation since: +# 1. System user ranges (100-999) can collide with host system users +# (see: https://github.com/nginxinc/docker-nginx/issues/490) +# 2. There's no value added and warning messages can be raised at build time +# (see: https://github.com/dotnet/dotnet-docker/issues/4624) +# +# The high UID/GID values provide an additional security boundary in containers +# where user namespaces are shared with the host. +ARG UID +ENV UID=${UID} +ARG GID +ENV GID=${GID} +ARG USER +ENV USER=${USER} +ARG HOME +ENV HOME=${HOME} + +COPY --chmod=550 <<-EOF /etc/passwd + root:x:0:0:root:/root:/bin/sh + user:x:${UID}:${GID}::${HOME}:/bin/sh +EOF + +COPY --chmod=550 <<-EOF /etc/group + root:x:0: + user:x:${GID}: +EOF + +WORKDIR /usr/local/bin + +USER root +RUN mkdir -p /usr/local/bin/wallets && chown -R ${UID}:${GID} /usr/local/bin/ && chmod -R 770 /usr/local/bin/ +COPY --chown=${UID}:${GID} --from=export /zingo-cli /usr/local/bin/zingo-cli +RUN chmod 550 /usr/local/bin/zingo-cli +COPY --chown=${UID}:${GID} ./utils/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod 550 /usr/local/bin/entrypoint.sh + +USER $USER +# ./entrypoint.sh runs, then executes CMD (or custom command if provided). +# Prints zingo-cli version, address if a new wallet is created, and info on success. +ENTRYPOINT [ "./entrypoint.sh" ] +CMD [ "./zingo-cli", "--help"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..f022103244 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +# Simple wrapper for scripts with printed status messages. +# +# Running `make` or `make stagex` will leverage the steps below +# to check compatibility and build the binaries via StageX. + +.PHONY: stagex compat build load create interact + +stagex: compat build + @echo "[Stageˣ] build completed via make." + +compat: + @echo "Beginning Compatibility Check step." + @./utils/compat.sh + @echo " [PASS] Compatibility Check passed." + +build: + @echo "Entering Build step." + @./utils/build.sh + @echo "Build step complete." + +load: + @echo "Attempting to load OCI image into local docker image store." + @./utils/load_image.sh + @echo "make load step complete." diff --git a/README.md b/README.md index 5687094ed2..7c556a9d48 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,29 @@ cargo build --release --package zingo-cli This will launch the interactive prompt. Type `help` to get a list of commands. +## Reproducible builds via StageX + +A bootstrapped and reproducible build pipeline using StageX is included in this repo. +If you meet all the compatibility requirements, to create `zingo-cli`, you can run +`make` in the root directory. The resulting binary will be found in the `/build/` +directory, along with an OCI image in the form of a tar ball. + +This image can be loaded into docker with the `make load` convenience script, and +contains the `zingo-cli` binary. + +To run interactively with a custom server: +`docker run -it zingo-cli:latest ./zingo-cli --server https://zzz.stripest.online:443` + +`zingo-cli` runs with several defaults. Importantly, these include a data-dir +with wallet file, which are created if they don't already exist: +a `wallets` dir in location where executable is run, containing the wallet +(`zingo-wallet.dat`) file. Other defaults inlcude setting the chain to mainnet, +using a default lightwallet server, using clearnet for price fetching, and not +executing commands prior to a complete chain sync. + +Any `docker run` will initialize a wallet if there was none in the container, and +by default prints info and then help if no arguments are passed. + ## Notes: * If you want to run your own server, please see [zingo lightwalletd](https://github.com/zingolabs/lightwalletd), and then run `./zingo-cli --server http://127.0.0.1:9067` * The default log file is in `~/.zcash/zingo-wallet.debug.log`. A default wallet is stored in `~/.zcash/zingo-wallet.dat` diff --git a/utils/build.sh b/utils/build.sh new file mode 100755 index 0000000000..6683851510 --- /dev/null +++ b/utils/build.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +DIR="$( cd "$( dirname "$0" )" && pwd )" +REPO_ROOT="$(git rev-parse --show-toplevel)" +PLATFORM="linux/amd64" +OCI_OUTPUT="$REPO_ROOT/build/oci" +DOCKERFILE="$REPO_ROOT/Dockerfile" + +export DOCKER_BUILDKIT=1 +export SOURCE_DATE_EPOCH=1 + +echo $DOCKERFILE +mkdir -p $OCI_OUTPUT + +# Build runtime image for docker run +echo "Building runtime image..." +docker build -f "$DOCKERFILE" "$REPO_ROOT" \ + --platform "$PLATFORM" \ + --target runtime \ + --output type=oci,rewrite-timestamp=true,force-compression=true,dest=$OCI_OUTPUT/zingo-cli.tar,name=zingo-cli \ + "$@" + +# Extract binary locally from export stage +echo "Extracting binary..." +docker build -f "$DOCKERFILE" "$REPO_ROOT" --quiet \ + --platform "$PLATFORM" \ + --target export \ + --output type=local,dest="$REPO_ROOT/build" \ + "$@" diff --git a/utils/compat.sh b/utils/compat.sh new file mode 100755 index 0000000000..8dcbba6048 --- /dev/null +++ b/utils/compat.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -e +readonly MIN_BASH_VERSION=5 +readonly MIN_DOCKER_VERSION=26.0.0 +readonly MIN_BUILDX_VERSION=0.13 +### Exit with error message +die() { + echo "$@" >&2 + exit 1 +} +### Bail and instruct user on missing package to install for their platform +die_pkg() { + local -r package=${1?} + local -r version=${2?} + local install_cmd + case "$OSTYPE" in + linux*) + if command -v "apt" >/dev/null; then + install_cmd="apt install ${package}" + elif command -v "yum" >/dev/null; then + install_cmd="yum install ${package}" + elif command -v "pacman" >/dev/null; then + install_cmd="pacman -Ss ${package}" + elif command -v "emerge" >/dev/null; then + install_cmd="emerge ${package}" + elif command -v "nix-env" >/dev/null; then + install_cmd="nix-env -i ${package}" + fi + ;; + bsd*) install_cmd="pkg install ${package}" ;; + darwin*) install_cmd="port install ${package}" ;; + *) die "Error: Your operating system is not supported" ;; + esac + echo "Error: ${package} ${version}+ does not appear to be installed." >&2 + [ -n "$install_cmd" ] && echo "Try: \`${install_cmd}\`" >&2 + exit 1 +} +### Check if actual binary version is >= minimum version +check_version(){ + local pkg="${1?}" + local have="${2?}" + local need="${3?}" + local i ver1 ver2 IFS='.' + [[ "$have" == "$need" ]] && return 0 + read -r -a ver1 <<< "$have" + read -r -a ver2 <<< "$need" + for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); + do ver1[i]=0; + done + for ((i=0; i<${#ver1[@]}; i++)); do + [[ -z ${ver2[i]} ]] && ver2[i]=0 + ((10#${ver1[i]} > 10#${ver2[i]})) && return 0 + ((10#${ver1[i]} < 10#${ver2[i]})) && die_pkg "${pkg}" "${need}" + done +} +### Check if required binaries are installed at appropriate versions +check_tools(){ + if [ -z "${BASH_VERSINFO[0]}" ] \ + || [ "${BASH_VERSINFO[0]}" -lt "${MIN_BASH_VERSION}" ]; then + die_pkg "bash" "${MIN_BASH_VERSION}" + fi + for cmd in "$@"; do + case $cmd in + buildx) + docker buildx version >/dev/null 2>&1 || die "Error: buildx not found" + version=$(docker buildx version 2>/dev/null | grep -o 'v[0-9.]*' | sed 's/v//') + check_version "buildx" "${version}" "${MIN_BUILDX_VERSION}" + ;; + docker) + command -v docker >/dev/null || die "Error: docker not found" + version=$(docker version -f '{{ .Server.Version }}') + check_version "docker" "${version}" "${MIN_DOCKER_VERSION}" + ;; + esac + done +} +check_tools docker buildx; +docker info -f '{{ .DriverStatus }}' \ + | grep "io.containerd.snapshotter.v1" >/dev/null \ +|| die "Error: Docker Engine is not using containerd for image storage" diff --git a/utils/entrypoint.sh b/utils/entrypoint.sh new file mode 100755 index 0000000000..2636a6310f --- /dev/null +++ b/utils/entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# Entrypoint for running zingo-cli in Docker. +# +# The main script logic is at the bottom. + +set -eo pipefail + +# Currently there is no support for running tests in-container, due to +# requiring additional binaries. +# +# Main Script Logic +# +# 1. Print environment variables and config for debugging. +# 2. Tests if zingo-cli runs. +# 3. Creates a wallet, if the container has not been initialized before. +# 4. Process command-line arguments and execute appropriate action. + +echo "INFO: Using the following environment variables:" +printenv + +echo "Testing zingo-cli to print version string:" +./zingo-cli --version + +if [ ! -f ./initialized ]; then + # A wallet will be created in this container if there is none. The address of the new wallet will be printed after sync." + echo "Container not initialized, creating wallet, syncing, and printing address..." + # selected server = zebra 4.1.0 and lwd v0.4.18-9-gb932e8e at time of commit + ./zingo-cli --server https://zzz.stripest.online:443 --waitsync addresses + touch ./initialized +fi + +echo "Lightwalletd server's info info:" +./zingo-cli --server https://zzz.stripest.online:443 --nosync info + +echo "now exec'ing $@ " +exec "$@" diff --git a/utils/load_image.sh b/utils/load_image.sh new file mode 100755 index 0000000000..16002e98f7 --- /dev/null +++ b/utils/load_image.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel)" +OCI_OUTPUT="$REPO_ROOT/build/oci" +TARBALL="${OCI_OUTPUT}/zingo-cli.tar" + +# Build runtime image for docker run +echo "Checking if the OCI output from build is present." +if [ -f "$TARBALL" ]; +then + echo "OCI output file present, loading tar file into local docker image store." + docker load < $TARBALL + echo "...Done!" +else + echo "OCI output file not present." +fi