git / moonbite-core / .github/workflows/release.yml · raw

# 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 binarieson:  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 <file> --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 <file> --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"