# Build MoonBite node binaries for Linux, Windows and macOS on every version # tag, prove the Linux build is reproducible, attest every artifact with # Sigstore (SLSA provenance), optionally minisign the checksum manifest, and # publish everything as a GitHub Release. # # Why this exists: the RandomX proof-of-work library is vendored into the tree # as a prebuilt Linux .a, so a plain checkout can only build for Linux. This # workflow rebuilds RandomX from source (pinned to the version whose hash was # verified byte-identical to the vendored library) for each target, then builds # litecoind/litecoin-cli with the depends system. macOS in particular cannot be # cross-built from Linux (Apple SDK licensing); GitHub's macOS runners build it # natively. # # Trust model (doc/reproducible-builds.md): the operator is pseudonymous and # cannot obtain an Authenticode or Apple notarization identity, so release # trust rests on four independent legs instead of a vendor certificate: # 1. deterministic builds - SOURCE_DATE_EPOCH, -ffile-prefix-map, # deterministic strip, fixed-mtime/sorted archives # 2. a rebuild-and-compare job on a separate runner that FAILS the release # when the Linux archive hash differs (a real reproducibility check, not # a claim) # 3. GitHub artifact attestations (actions/attest-build-provenance -> Sigstore, # SLSA build provenance bound to this repo, this workflow, this commit) # 4. minisign signature over SHA256SUMS.txt, skipped cleanly while the # MINISIGN_SECRET_KEY secret is absent (the public key is still a # placeholder in moonbite-network/deploy/moonbite-release.pub) # # contrib/guix (inherited from Litecoin/Bitcoin Core) is the gold-standard # bootstrappable path and is the long-term target; it is documented, not run # here, because a Guix build of all hosts takes many hours and needs a # dedicated builder. See the `guix` job stub at the bottom. # # Every third-party action is pinned to a full commit SHA; the trailing comment # is the tag it was resolved from. name: Release binaries on: push: tags: - "v*" workflow_dispatch: {} env: # RandomX pinned to the version whose hash matches the vendored librandomx.a # (verified: the SHA-256 seed test produced byte-identical output). Bumping # this is a consensus change - do not do it casually. RANDOMX_TAG: "v1.2.1" # Commit that tag v1.2.1 points at. Tags are mutable; this is what is built. RANDOMX_SHA: "102f8acf90a7649ada410de5499a7ec62e49e1da" # Repo holding the miner scripts (mine.sh / mine.ps1 / README) shipped with # the binaries, so there is one source of truth for them. MINER_SCRIPTS_REPO: "moonbitecoin/moonbite-network" # Pinned commit of MINER_SCRIPTS_REPO (origin/main at pin time). Bump # deliberately after reviewing the miner-script diff. MINER_SCRIPTS_SHA: "090626567e1844be475e1dce1c2411cfd3211fd3" # Deterministic build: every path baked into a binary (assert strings, # __FILE__, debug info) is rewritten to this prefix so the runner's checkout # directory never leaks into the output. BUILD_PREFIX_MAP_TO: "/build/moonbite" TZ: "UTC" LC_ALL: "C" jobs: build: name: "build (${{ matrix.name }}${{ matrix.rebuild && ', rebuild' || '' }})" runs-on: "${{ matrix.runs-on }}" strategy: fail-fast: false matrix: include: - name: linux-x86_64 runs-on: ubuntu-22.04 host: x86_64-pc-linux-gnu exe: "" archive: tar.gz rebuild: false # Second, independent build of the Linux target on a different runner # VM. The `reproducibility` job compares its hash to the first one. - name: linux-x86_64 runs-on: ubuntu-22.04 host: x86_64-pc-linux-gnu exe: "" archive: tar.gz rebuild: true - name: windows-x86_64 runs-on: ubuntu-22.04 host: x86_64-w64-mingw32 exe: ".exe" archive: zip rebuild: false - name: macos-x86_64 runs-on: macos-13 host: x86_64-apple-darwin exe: "" archive: tar.gz rebuild: false - name: macos-arm64 runs-on: macos-14 host: arm64-apple-darwin exe: "" archive: tar.gz rebuild: false steps: - name: Checkout node source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 # SOURCE_DATE_EPOCH is honoured by GCC/Clang (__DATE__/__TIME__), by # libtool/ar, and by our own archive steps below. It is derived from the # commit being built, so two builds of the same commit agree and a build # of a different commit does not silently inherit a stale value. - name: Set SOURCE_DATE_EPOCH from the commit timestamp shell: bash run: | set -e EPOCH="$(git log -1 --pretty=%ct)" test -n "$EPOCH" echo "SOURCE_DATE_EPOCH=$EPOCH" >> "$GITHUB_ENV" echo "source date epoch: $EPOCH ($(date -u -d "@$EPOCH" 2>/dev/null || date -u -r "$EPOCH"))" - name: Install build tooling (Linux and Windows-cross) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y build-essential libtool autotools-dev automake \ pkg-config bsdmainutils curl git ca-certificates cmake python3 zip if [ "${{ matrix.host }}" = "x86_64-w64-mingw32" ]; then sudo apt-get install -y g++-mingw-w64-x86-64 # Litecoin requires the POSIX threading variant of the mingw toolchain. sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix fi # gnu-tar: bsdtar on macOS lacks --sort/--mtime/--numeric-owner, which the # deterministic archive step relies on. - name: Install build tooling (macOS) if: runner.os == 'macOS' run: brew install automake libtool pkg-config cmake gnu-tar - name: "Build RandomX for ${{ matrix.host }}" run: | set -e git init -q rx git -C rx fetch --depth 1 https://github.com/tevador/RandomX.git "$RANDOMX_SHA" git -C rx checkout -q FETCH_HEAD test "$(git -C rx rev-parse HEAD)" = "$RANDOMX_SHA" \ || { echo "RandomX checkout is not $RANDOMX_SHA"; exit 1; } mkdir rx/build && cd rx/build # Same path-prefix rewrite as the node build so librandomx.a carries # no runner-specific paths. RX_FLAGS="-ffile-prefix-map=$GITHUB_WORKSPACE/rx=$BUILD_PREFIX_MAP_TO/rx" case "${{ matrix.host }}" in x86_64-w64-mingw32) cmake -DCMAKE_SYSTEM_NAME=Windows \ -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \ -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \ -DCMAKE_RC_COMPILER=x86_64-w64-mingw32-windres \ -DCMAKE_C_FLAGS="$RX_FLAGS" -DCMAKE_CXX_FLAGS="$RX_FLAGS" \ -DCMAKE_BUILD_TYPE=Release .. ;; arm64-apple-darwin) cmake -DCMAKE_OSX_ARCHITECTURES=arm64 -DARCH=default \ -DCMAKE_C_FLAGS="$RX_FLAGS" -DCMAKE_CXX_FLAGS="$RX_FLAGS" \ -DCMAKE_BUILD_TYPE=Release .. ;; x86_64-apple-darwin) cmake -DCMAKE_OSX_ARCHITECTURES=x86_64 -DARCH=default \ -DCMAKE_C_FLAGS="$RX_FLAGS" -DCMAKE_CXX_FLAGS="$RX_FLAGS" \ -DCMAKE_BUILD_TYPE=Release .. ;; *) cmake -DARCH=default \ -DCMAKE_C_FLAGS="$RX_FLAGS" -DCMAKE_CXX_FLAGS="$RX_FLAGS" \ -DCMAKE_BUILD_TYPE=Release .. ;; esac make -j"$(getconf _NPROCESSORS_ONLN)" randomx # Rewrite the static archive deterministically (zero uid/gid/mtime). if [ "${{ runner.os }}" = "Linux" ]; then if [ "${{ matrix.host }}" = "x86_64-w64-mingw32" ]; then x86_64-w64-mingw32-ranlib -D librandomx.a else ranlib -D librandomx.a fi fi cp librandomx.a "$GITHUB_WORKSPACE/src/crypto/randomx/librandomx.a" - name: Cache depends uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: "depends/${{ matrix.host }}" key: "depends-${{ matrix.host }}-${{ hashFiles('depends/packages/*.mk', 'depends/*.mk') }}" - name: Build depends (NO_QT) run: | make -C depends HOST=${{ matrix.host }} NO_QT=1 \ -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu)" - name: Configure and build node run: | set -e ./autogen.sh # Linux binaries carry RUNPATH=/opt/moonbite/lib so the seed nodes (Ubuntu # 24.04 hosting the 22.04 runtime libs there) never need patchelf on-host. # Harmless elsewhere: a RUNPATH is only searched, and the depends build is # static anyway. See doc/linux-runpath.md. EXTRA_LDFLAGS="" if [ "${{ matrix.host }}" = "x86_64-pc-linux-gnu" ]; then EXTRA_LDFLAGS="-Wl,-rpath,/opt/moonbite/lib" fi # Determinism: # -ffile-prefix-map rewrites the checkout path in __FILE__, asserts # and debug info (covers -fdebug-prefix-map and # -fmacro-prefix-map). # -frandom-seed is deliberately NOT set: GCC requires a distinct seed # per translation unit for it to be safe, and the # default seed is already derived from the input, # so a single global value would add collision risk # without adding determinism. # The optimisation level is restated because passing CXXFLAGS to # configure replaces its default "-g -O2". DET_FLAGS="-O2 -ffile-prefix-map=$GITHUB_WORKSPACE=$BUILD_PREFIX_MAP_TO" CONFIG_SITE="$PWD/depends/${{ matrix.host }}/share/config.site" \ ./configure --prefix=/ --disable-tests --disable-bench --with-gui=no \ CFLAGS="$DET_FLAGS" CXXFLAGS="$DET_FLAGS" \ LDFLAGS="$EXTRA_LDFLAGS" make -C src \ -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu)" \ "litecoind${{ matrix.exe }}" "litecoin-cli${{ matrix.exe }}" - name: Assemble bundle (deterministic) shell: bash run: | set -e D=moonbite-miner mkdir -p "$D" cp "src/litecoind${{ matrix.exe }}" "$D/moonbited${{ matrix.exe }}" cp "src/litecoin-cli${{ matrix.exe }}" "$D/moonbite-cli${{ matrix.exe }}" # -D: deterministic mode (zero timestamps/uid/gid in any archive # members; no-op but harmless on a plain executable). Not supported by # Apple's strip; macOS binaries are stripped at link time by the # toolchain and left as produced. if [ "${{ matrix.host }}" = "x86_64-w64-mingw32" ]; then x86_64-w64-mingw32-strip -D "$D"/moonbited.exe "$D"/moonbite-cli.exe elif [ "${{ runner.os }}" = "Linux" ]; then strip -D "$D"/moonbited "$D"/moonbite-cli fi git init -q webrepo git -C webrepo fetch --depth 1 "https://github.com/${MINER_SCRIPTS_REPO}.git" "$MINER_SCRIPTS_SHA" git -C webrepo checkout -q FETCH_HEAD test "$(git -C webrepo rev-parse HEAD)" = "$MINER_SCRIPTS_SHA" \ || { echo "miner-scripts checkout is not $MINER_SCRIPTS_SHA"; exit 1; } cp webrepo/release/miner/mine.sh webrepo/release/miner/mine.ps1 \ webrepo/release/miner/README.md "$D/" # Normalise everything the archive will record: mode, mtime. find "$D" -type f -exec chmod 0644 {} + chmod 0755 "$D"/mine.sh "$D"/moonbited* "$D"/moonbite-cli* find "$D" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} + OUT="moonbite-miner-${{ matrix.name }}" if [ "${{ matrix.archive }}" = "zip" ]; then # zip: sorted member list on stdin, -X drops the uid/gid/atime # "extra" fields, TZ=UTC fixes the DOS timestamp conversion. find "$D" -type f | LC_ALL=C sort | zip -X -q -9 "$OUT.zip" -@ else TAR=tar command -v gtar >/dev/null 2>&1 && TAR=gtar # gzip -n drops the original filename/timestamp from the gzip header. "$TAR" --sort=name --mtime="@$SOURCE_DATE_EPOCH" \ --owner=0 --group=0 --numeric-owner \ --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ -cf - "$D" | gzip -n -9 > "$OUT.tar.gz" fi sha256sum "$OUT".* 2>/dev/null || shasum -a 256 "$OUT".* - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: # The rebuild pass is uploaded under a name the release job's glob # does not match; only the `reproducibility` job consumes it. name: "${{ matrix.rebuild && 'rebuild-' || 'moonbite-miner-' }}${{ matrix.name }}" path: "moonbite-miner-${{ matrix.name }}.*" if-no-files-found: error reproducibility: name: "Reproducibility check (linux-x86_64)" needs: build runs-on: ubuntu-22.04 steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: moonbite-miner-linux-x86_64 path: a - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: rebuild-linux-x86_64 path: b - name: Compare the two independent builds run: | set -e echo "== build 1"; (cd a && sha256sum *) echo "== build 2"; (cd b && sha256sum *) A="$(cd a && sha256sum moonbite-miner-linux-x86_64.tar.gz | cut -d' ' -f1)" B="$(cd b && sha256sum moonbite-miner-linux-x86_64.tar.gz | cut -d' ' -f1)" if [ "$A" != "$B" ]; then echo "::error::Linux build is NOT reproducible: $A != $B" echo "Per-member diff:" mkdir xa xb tar -xzf a/moonbite-miner-linux-x86_64.tar.gz -C xa tar -xzf b/moonbite-miner-linux-x86_64.tar.gz -C xb (cd xa && find . -type f | sort | xargs sha256sum) > sums-a.txt (cd xb && find . -type f | sort | xargs sha256sum) > sums-b.txt diff sums-a.txt sums-b.txt || true exit 1 fi echo "REPRODUCIBLE: both builds hash to $A" release: name: Publish release needs: [build, reproducibility] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-22.04 permissions: contents: write # create the Release and upload assets id-token: write # Sigstore OIDC identity for the attestation attestations: write # store the attestation on this repo env: # The `secrets` context is not available in step `if:` expressions; expose # presence (never the value) as a boolean the steps can test. HAVE_MINISIGN_KEY: ${{ secrets.MINISIGN_SECRET_KEY != '' }} steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: "moonbite-miner-*" path: dist merge-multiple: true - name: Checksums run: | cd dist LC_ALL=C sha256sum moonbite-miner-* > SHA256SUMS.txt cat SHA256SUMS.txt # Sigstore / SLSA provenance. Verify with: # gh attestation verify --owner moonbitecoin # The attestation binds the artifact digest to this repository, this # workflow file, the tag and the commit, signed with GitHub's OIDC # identity for the run. It proves "built by this workflow from this # commit"; it does not prove the source is honest - that is what the # reproducibility job and public code review are for. - name: Attest build provenance (Sigstore) uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | dist/moonbite-miner-* dist/SHA256SUMS.txt # Optional minisign signature over the manifest. Skips cleanly while the # MINISIGN_SECRET_KEY secret is absent. The key is expected to be a # minisign secret key file body; if MINISIGN_PASSWORD is also set it is # fed on stdin, otherwise the key must have been created with # `minisign -G -W` (unencrypted). A CI-held key is weaker than the offline # key described in moonbite-network/deploy/RELEASE-SIGNING.md; the # attestation above remains the primary proof. - name: Sign SHA256SUMS.txt with minisign (optional) if: env.HAVE_MINISIGN_KEY == 'true' env: MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} run: | set -euo pipefail sudo apt-get update -qq && sudo apt-get install -y -qq minisign umask 077 KEY="$RUNNER_TEMP/minisign.key" printf '%s\n' "$MINISIGN_SECRET_KEY" > "$KEY" TC="MoonBite ${GITHUB_REF_NAME} SHA256SUMS.txt, commit ${GITHUB_SHA}" if [ -n "${MINISIGN_PASSWORD:-}" ]; then printf '%s\n' "$MINISIGN_PASSWORD" | minisign -S -s "$KEY" -t "$TC" -m dist/SHA256SUMS.txt else minisign -S -W -s "$KEY" -t "$TC" -m dist/SHA256SUMS.txt fi rm -f "$KEY" echo "signed -> dist/SHA256SUMS.txt.minisig" cat dist/SHA256SUMS.txt.minisig - name: Note unsigned manifest if: env.HAVE_MINISIGN_KEY != 'true' run: | echo "::notice::MINISIGN_SECRET_KEY not configured; SHA256SUMS.txt is published unsigned." \ "Sigstore attestation and the reproducibility check still apply." - name: Create GitHub Release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: dist/* generate_release_notes: true body: | MoonBite node and solo miner, all platforms. Every binary embeds RandomX ${{ env.RANDOMX_TAG }} and the MoonBite 32-byte proof-of-work seed. Verify what you downloaded (see doc/reproducible-builds.md): - `sha256sum -c SHA256SUMS.txt --ignore-missing` - `gh attestation verify --owner moonbitecoin` (Sigstore/SLSA provenance) - `minisign -Vm SHA256SUMS.txt -p moonbite-release.pub` when a `.minisig` is attached - The Linux archive was built twice on separate runners in this run and the hashes matched; rebuild it yourself from commit ${{ github.sha }} to confirm independently. Mining is solo and pool-free: unzip, then run mine.sh (Linux/macOS) or mine.ps1 (Windows) and the node mines to your own wallet. See the bundled README. # Stub: bootstrappable Guix build (contrib/guix, inherited from Litecoin # Core). This is the gold-standard reproducible path because the entire # toolchain is itself built from source with pinned hashes, removing the # runner image from the trust base. It is intentionally disabled: a full # Guix build of all hosts takes many hours and needs a persistent builder # with a Guix store. Enable by removing `if: false` once a self-hosted # builder exists, or run `contrib/guix/guix-build.sh` locally and compare # the resulting hashes with SHA256SUMS.txt from this workflow. guix: name: "Guix build (stub, disabled)" if: false runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - run: | echo "See contrib/guix/README.md. Example:" echo " HOSTS='x86_64-linux-gnu x86_64-w64-mingw32' ./contrib/guix/guix-build.sh"