Skip to content

Commit dcb5ba5

Browse files
committed
build: reproducible build and signed release digest
Closes #2.
1 parent 3a5340a commit dcb5ba5

10 files changed

Lines changed: 414 additions & 31 deletions

File tree

.github/workflows/release.yaml

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ jobs:
9696
- name: Checkout
9797
uses: actions/checkout@v4
9898

99+
- name: Get reproducible build timestamp
100+
id: source_date
101+
run: echo "epoch=$(git log -1 --pretty=%ct)" >> $GITHUB_OUTPUT
102+
99103
- name: Set up Go
100104
uses: actions/setup-go@v5
101105
with:
@@ -112,22 +116,23 @@ jobs:
112116
GOOS: ${{ matrix.goos }}
113117
GOARCH: ${{ matrix.goarch }}
114118
CGO_ENABLED: 0
119+
SOURCE_DATE_EPOCH: ${{ steps.source_date.outputs.epoch }}
115120
run: |
116121
VERSION=${{ needs.create-release.outputs.version }}
117-
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
118-
COMMIT_HASH=$(git rev-parse --short HEAD)
119122
120123
go build \
121-
-ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} -X main.commit=${COMMIT_HASH}" \
124+
-trimpath \
125+
-buildvcs=false \
126+
-ldflags="-buildid= -s -w -X main.version=${VERSION}" \
122127
-o ${{ matrix.output }} \
123128
./cmd/neutrinod
124129
125-
- name: Upload Release Asset
126-
env:
127-
GH_TOKEN: ${{ github.token }}
128-
run: |
129-
gh release upload ${{ needs.create-release.outputs.version }} \
130-
"neutrino_server/${{ matrix.output }}"
130+
- name: Upload binary artifact
131+
uses: actions/upload-artifact@v4
132+
with:
133+
name: binary-${{ matrix.output }}
134+
path: neutrino_server/${{ matrix.output }}
135+
if-no-files-found: error
131136

132137
build-docker:
133138
needs: create-release
@@ -189,26 +194,42 @@ jobs:
189194
- name: Checkout
190195
uses: actions/checkout@v4
191196

192-
- name: Download release assets
193-
env:
194-
GH_TOKEN: ${{ github.token }}
195-
run: |
196-
mkdir -p artifacts
197-
cd artifacts
198-
gh release download ${{ needs.create-release.outputs.version }} \
199-
--repo ${{ github.repository }} \
200-
--pattern 'neutrinod-*'
197+
- name: Download built binaries
198+
uses: actions/download-artifact@v4
199+
with:
200+
pattern: binary-*
201+
path: artifacts
202+
merge-multiple: true
201203

202204
- name: Generate checksums
203205
run: |
204206
cd artifacts
205-
sha256sum * > SHA256SUMS
207+
sha256sum neutrinod-* | sort > SHA256SUMS
206208
cat SHA256SUMS
207209
208-
- name: Upload checksums
210+
- name: Verify signed checksums from repository
211+
run: |
212+
VERSION=${{ needs.create-release.outputs.version }}
213+
EXPECTED_DIR="signatures/${VERSION}"
214+
215+
test -f "${EXPECTED_DIR}/SHA256SUMS"
216+
test -f "${EXPECTED_DIR}/SHA256SUMS.asc"
217+
218+
diff -u "${EXPECTED_DIR}/SHA256SUMS" "artifacts/SHA256SUMS"
219+
220+
for pubkey in signatures/pubkeys/*.asc; do
221+
gpg --import "$pubkey"
222+
done
223+
224+
gpg --verify "${EXPECTED_DIR}/SHA256SUMS.asc" "artifacts/SHA256SUMS"
225+
226+
- name: Upload binaries, checksums, and signature
209227
env:
210228
GH_TOKEN: ${{ github.token }}
211229
run: |
230+
VERSION=${{ needs.create-release.outputs.version }}
212231
gh release upload ${{ needs.create-release.outputs.version }} \
232+
./artifacts/neutrinod-* \
213233
./artifacts/SHA256SUMS \
234+
"./signatures/${VERSION}/SHA256SUMS.asc" \
214235
--repo ${{ github.repository }}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add reproducible release flow for `neutrinod` binaries with deterministic Go build flags and `SOURCE_DATE_EPOCH`.
13+
- Add `scripts/release-build-sign.sh` to build release binaries locally, generate `SHA256SUMS`, and create a detached GPG signature.
14+
- Add `scripts/verify-release-build.sh` as a one-command local reproducibility check against the signed digest.
15+
- Add release signature infrastructure under `signatures/` with trusted key list and m0wer public key.
1216
- Add `addPeers` setting, similar to `connectPeers`, that allows specifying peers to connect to without disabling peer discovery.
1317

18+
### Changed
19+
20+
- Update release workflow to rebuild binaries in CI, verify checksums against committed `signatures/<version>/SHA256SUMS`, verify `SHA256SUMS.asc`, and upload binaries plus signed digest files to GitHub releases.
21+
1422
## [0.7.0] - 2026-03-11
1523

1624
### Added

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ go build -o neutrinod ./cmd/neutrinod
6161
./neutrinod --network=mainnet --listen=0.0.0.0:8334
6262
```
6363

64+
## Reproducible Releases
65+
66+
Release binaries are reproducible and tied to a signed digest:
67+
68+
1. Build locally and sign checksums:
69+
70+
```bash
71+
./scripts/release-build-sign.sh v1.0.0 --key 1C53A412D11EF3051704419C44912E1E03005B31
72+
```
73+
74+
2. Commit `signatures/v1.0.0/SHA256SUMS` and `signatures/v1.0.0/SHA256SUMS.asc`.
75+
3. Push the release tag (`v1.0.0`).
76+
77+
The release workflow rebuilds all binaries, verifies the resulting `SHA256SUMS` exactly matches the committed digest, verifies the GPG signature using keys in `signatures/pubkeys/`, and uploads binaries + `SHA256SUMS` + `SHA256SUMS.asc` to the GitHub release.
78+
79+
Anyone can reproduce and verify a release locally with one command:
80+
81+
```bash
82+
./scripts/verify-release-build.sh v1.0.0
83+
```
84+
6485
## Configuration
6586

6687
### Environment Variables

RELEASE.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@ git commit -m "Prepare release v1.0.0"
2222
git push origin main
2323
```
2424

25-
### 2. Create and Push Tag
25+
### 2. Build Reproducible Binaries and Sign Digest
26+
27+
```bash
28+
# Build deterministic binaries and sign checksum file
29+
./scripts/release-build-sign.sh v1.0.0 --key 1C53A412D11EF3051704419C44912E1E03005B31
30+
31+
# Commit signed digest artifacts
32+
git add signatures/v1.0.0/
33+
git commit -m "Add signed checksums for v1.0.0"
34+
git push origin main
35+
```
36+
37+
### 3. Create and Push Tag
2638

2739
```bash
2840
# Create annotated tag
@@ -40,7 +52,7 @@ Changes:
4052
git push origin v1.0.0
4153
```
4254

43-
### 3. Monitor Release
55+
### 4. Monitor Release
4456

4557
The GitHub Actions workflow will automatically:
4658

@@ -54,8 +66,9 @@ The GitHub Actions workflow will automatically:
5466
- linux/arm64
5567

5668
3. **Create GitHub Release** with:
57-
- All binary archives
69+
- All binaries
5870
- SHA256SUMS file
71+
- SHA256SUMS.asc signature
5972
- Release notes
6073

6174
4. **Push Docker Images** to:
@@ -66,23 +79,33 @@ The GitHub Actions workflow will automatically:
6679

6780
Monitor at: `https://github.com/yourusername/neutrino-api/actions`
6881

69-
### 4. Verify Release
82+
### 5. Verify Release
7083

7184
```bash
7285
# Check GitHub release page
7386
open https://github.com/yourusername/neutrino-api/releases
7487

7588
# Test binary download
76-
wget https://github.com/yourusername/neutrino-api/releases/download/v1.0.0/neutrinod-linux-amd64.tar.gz
77-
tar -xzf neutrinod-linux-amd64.tar.gz
89+
wget https://github.com/yourusername/neutrino-api/releases/download/v1.0.0/neutrinod-linux-amd64
90+
chmod +x neutrinod-linux-amd64
7891
./neutrinod-linux-amd64 --version
7992

93+
# Verify signed checksums
94+
wget https://github.com/yourusername/neutrino-api/releases/download/v1.0.0/SHA256SUMS
95+
wget https://github.com/yourusername/neutrino-api/releases/download/v1.0.0/SHA256SUMS.asc
96+
gpg --import signatures/pubkeys/1C53A412D11EF3051704419C44912E1E03005B31.asc
97+
gpg --verify SHA256SUMS.asc SHA256SUMS
98+
sha256sum -c SHA256SUMS
99+
100+
# Reproduce all binaries locally and compare to the signed digest
101+
./scripts/verify-release-build.sh v1.0.0
102+
80103
# Test Docker image
81104
docker pull ghcr.io/yourusername/neutrino-api:v1.0.0
82105
docker run --rm ghcr.io/yourusername/neutrino-api:v1.0.0 neutrinod --version
83106
```
84107

85-
### 5. Announce Release
108+
### 6. Announce Release
86109

87110
- Update project README if needed
88111
- Post in relevant communities

VERSIONING.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,18 @@ var (
5151
)
5252
```
5353

54-
### 2. Create a Tag
54+
### 2. Build and Sign Release Digest
55+
56+
Before tagging, build artifacts locally and sign the checksum file:
57+
58+
```bash
59+
./scripts/release-build-sign.sh v1.0.0 --key 1C53A412D11EF3051704419C44912E1E03005B31
60+
git add signatures/v1.0.0/
61+
git commit -m "Add signed checksums for v1.0.0"
62+
git push origin main
63+
```
64+
65+
### 3. Create a Tag
5566

5667
Tags should follow the format `v<MAJOR>.<MINOR>.<PATCH>`:
5768

@@ -60,14 +71,21 @@ git tag -a v1.0.0 -m "Release v1.0.0 based on Neutrino v0.16.0"
6071
git push origin v1.0.0
6172
```
6273

63-
### 3. Automated Release
74+
### 4. Automated Release
6475

6576
The GitHub Actions workflow (`.github/workflows/release.yaml`) will:
6677

6778
1. Build binaries for multiple platforms (Linux, macOS, Windows on amd64/arm64)
6879
2. Create Docker images for multiple architectures
69-
3. Generate SHA256 checksums
70-
4. Create a GitHub release with all artifacts
80+
3. Regenerate SHA256 checksums and verify they exactly match `signatures/<version>/SHA256SUMS`
81+
4. Verify `signatures/<version>/SHA256SUMS.asc` with keys in `signatures/pubkeys/`
82+
5. Create a GitHub release with all artifacts, including `SHA256SUMS` and `SHA256SUMS.asc`
83+
84+
Users can reproduce and verify locally with:
85+
86+
```bash
87+
./scripts/verify-release-build.sh v1.0.0
88+
```
7189

7290
## Tracking Upstream Changes
7391

scripts/release-build-sign.sh

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
7+
8+
usage() {
9+
cat <<EOF
10+
Usage: $(basename "$0") <version> [--key <fingerprint-or-email>]
11+
12+
Build release binaries reproducibly, generate SHA256SUMS, and sign the digest.
13+
14+
Examples:
15+
$(basename "$0") v0.8.0
16+
$(basename "$0") v0.8.0 --key 1C53A412D11EF3051704419C44912E1E03005B31
17+
EOF
18+
}
19+
20+
if [[ $# -lt 1 ]]; then
21+
usage
22+
exit 1
23+
fi
24+
25+
VERSION="$1"
26+
shift
27+
28+
GPG_KEY=""
29+
while [[ $# -gt 0 ]]; do
30+
case "$1" in
31+
--key)
32+
GPG_KEY="$2"
33+
shift 2
34+
;;
35+
-h|--help)
36+
usage
37+
exit 0
38+
;;
39+
*)
40+
echo "Unknown argument: $1" >&2
41+
usage
42+
exit 1
43+
;;
44+
esac
45+
done
46+
47+
for cmd in git go gpg sha256sum; do
48+
if ! command -v "$cmd" >/dev/null 2>&1; then
49+
echo "Missing required command: $cmd" >&2
50+
exit 1
51+
fi
52+
done
53+
54+
SOURCE_DATE_EPOCH="$(git -C "$REPO_ROOT" log -1 --pretty=%ct)"
55+
BUILD_DIR="$REPO_ROOT/tmp/release-build/$VERSION"
56+
SIGNED_DIR="$REPO_ROOT/signatures/$VERSION"
57+
58+
rm -rf "$BUILD_DIR"
59+
mkdir -p "$BUILD_DIR"
60+
mkdir -p "$SIGNED_DIR"
61+
62+
declare -a TARGETS=(
63+
"linux amd64 neutrinod-linux-amd64"
64+
"linux arm64 neutrinod-linux-arm64"
65+
"darwin amd64 neutrinod-darwin-amd64"
66+
"darwin arm64 neutrinod-darwin-arm64"
67+
"windows amd64 neutrinod-windows-amd64.exe"
68+
)
69+
70+
for target in "${TARGETS[@]}"; do
71+
read -r GOOS GOARCH OUTPUT <<<"$target"
72+
echo "Building $OUTPUT"
73+
74+
(
75+
cd "$REPO_ROOT/neutrino_server"
76+
GOOS="$GOOS" \
77+
GOARCH="$GOARCH" \
78+
CGO_ENABLED=0 \
79+
SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
80+
go build \
81+
-trimpath \
82+
-buildvcs=false \
83+
-ldflags="-buildid= -s -w -X main.version=${VERSION}" \
84+
-o "$BUILD_DIR/$OUTPUT" \
85+
./cmd/neutrinod
86+
)
87+
done
88+
89+
(
90+
cd "$BUILD_DIR"
91+
sha256sum neutrinod-* | sort > SHA256SUMS
92+
)
93+
94+
cp "$BUILD_DIR/SHA256SUMS" "$SIGNED_DIR/SHA256SUMS"
95+
96+
SIGN_ARGS=(--armor --detach-sign)
97+
if [[ -n "$GPG_KEY" ]]; then
98+
SIGN_ARGS+=(--local-user "$GPG_KEY")
99+
fi
100+
101+
gpg "${SIGN_ARGS[@]}" \
102+
--output "$SIGNED_DIR/SHA256SUMS.asc" \
103+
"$SIGNED_DIR/SHA256SUMS"
104+
105+
gpg --verify "$SIGNED_DIR/SHA256SUMS.asc" "$SIGNED_DIR/SHA256SUMS"
106+
107+
echo
108+
echo "Release digest and signature created:"
109+
echo " $SIGNED_DIR/SHA256SUMS"
110+
echo " $SIGNED_DIR/SHA256SUMS.asc"
111+
echo
112+
echo "Temporary build outputs: $BUILD_DIR"

0 commit comments

Comments
 (0)