Documentation

Releases and verifying them

Status: the key is generated, the releases are signed.

The release public key:

RWRA0GxkCXP1xdvCABffa3Lu8XBFplqC9P7JSefgpCk/NSkZ6q9k4i4Y

It is in scripts/verify-release.sh too. The private half never leaves the machine that signs, and is not in CI — a key in a CI secret is a key anyone with push access can use.

The cheapest attack on a new chain is not breaking the algorithm. It is handing out a modified binary: it is enough to slip someone a node that sends the key elsewhere when a transaction is signed, or a miner that mines to somebody else’s address every hundredth block. That is why a release has two properties, and each solves something different:

what it proves what it does not prove
the signature the binary was released by the key holder that the source does what it claims
reproducibility the binary came from this source that the source is free of bugs

One without the other is half the story: a signature without reproducibility means “trust me”, and reproducibility without a signature means “trust whoever sent you that file”.

How a release is made

A release has four things people download — the all-in-one miner for Linux and for Windows, and the GUI wallet for Linux and for Windows — and three of them cannot be built on our own runners. Tauri links the system webview, so the Windows installer needs a Windows machine; nvcc drives MSVC there, so the GPU plugin for Windows needs one too. The release is therefore assembled by GitHub Actions and signed afterwards, away from CI:

git tag -s v0.1.0 && git push origin v0.1.0
#   → the `release` workflow builds everything and leaves a DRAFT prerelease
scripts/sign-release.sh v0.1.0
#   → signs SHA256SUMS and attaches the signature
gh release edit v0.1.0 --repo powmachina/powmachina --draft=false

The workflow calls scripts/release.sh … --no-sign for the binaries, so what CI produces comes out of the same script a release made by hand would use — two build paths would drift, and the difference would only show up in the checksums. The GUI installers and the Windows plugin come from their own jobs (wallet-gui, cuda-windows), which the release workflow calls rather than copies: the -gencode list of supported cards has to stay identical everywhere.

sign-release.sh checks that the files on the release match SHA256SUMS before signing, in both directions — a file added to the release after CI built it would otherwise be covered by a signature that never saw it.

Everything local, without GitHub (no GUI installers this way):

git tag -s v0.1.0 && scripts/release.sh v0.1.0

The script builds in a container pinned by digest, not by tag — golang:1.26 changes with every patch release and the binary’s bytes change with it. The build runs with -trimpath, -buildvcs=false and -ldflags="-s -w -buildid="; the modules are fetched in a separate step, so the build itself needs no network.

The source has to be clean (git status). A release from a work-in-progress tree cannot be reproduced from git, so it would not be a release but a file.

Signing is done with minisign (ed25519). GPG would mean explaining keyrings and the web of trust to people who only want to run a node; minisign has a one-line public key and a one-command verification. The private key is not in the repo or in CI — signing happens on the machine where it lives, not in a pipeline that every commit can start. That is the reason the workflow leaves a draft: a key in a CI secret is a key anyone with push access can use, and then the signature stops meaning what it is supposed to mean.

How whoever downloaded a binary verifies it

scripts/verify-release.sh ~/Downloads/powmachina-v0.1.0

Or by hand, in this order:

minisign -Vm SHA256SUMS -P <the public key>   # 1. the hash list is from us
sha256sum -c SHA256SUMS                        # 2. the files match the list

The other order is pointless: an unsigned hash list only proves that a file agrees with itself.

And whoever does not want to trust us at all builds it themselves and compares:

git checkout $(grep ^commit VERSION | cut -d' ' -f2)
scripts/release.sh v0.1.0 --check    # builds twice and compares the hashes

What is not reproducible and why

status
powmachinad, the wallet, the miner, the signer (linux-amd64) ✅ bit-exact in the pinned container
powmachinad.exe, the wallet, the miner (windows-amd64) ✅ cross-built through mingw in the same image
libpowm-cuda.so ❌ no — nvcc is not deterministic and the plugin is built on a machine with a card (rule 3: the kernel has to pass the test vectors, otherwise it is not released). The hash in SHA256SUMS says which file came out; it does not say that you will build it the same way from the same source.
macOS ❌ not there yet. A cross build with cgo needs the Apple SDK, and that cannot legally just be packed into a container.
powm-cuda.dll (GPU mining on Windows) ❌ not reproducible, for the same reason as the .so. It is built by the cuda-windows workflow (nvcc driving MSVC) and verified against the test vectors on a machine with a card before a release takes it. The files carry no Authenticode signature — Windows says “unknown publisher” and Smart App Control blocks them where it is on.
the quorum signer on Windows ❌ deliberately. A quorum key belongs on a machine that runs around the clock.
the images in the registry ❌ they are not a release. They are images for our own machines, and :latest moves.

What is not here is written down here deliberately. what to verify yourself is the list of admitted weak spots and this is one of them.