Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ jobs:
- run: npm ci
- run: npm run check
- run: npm run build
- name: Verify immutable images and repository hygiene
run: ./scripts/verify-release-inputs.sh
- name: Audit production dependencies
run: ./scripts/audit-build-dependencies.sh

secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan repository history for secrets
run: >-
docker run --rm -v "$PWD:/repo"
ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f
detect --source=/repo --redact --no-banner
64 changes: 64 additions & 0 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build release candidate

on:
workflow_dispatch:
push:
tags: ['v*-beta.*']

permissions:
contents: read
id-token: write
attestations: write

jobs:
package:
strategy:
matrix:
include:
- make_target: x86
artifact: gridpool_x86_64.s9pk
- make_target: arm
artifact: gridpool_aarch64.s9pk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: sudo apt-get update && sudo apt-get install -y squashfs-tools squashfs-tools-ng
- name: Install pinned StartOS CLI
run: |
curl -fsSL \
https://github.com/Start9Labs/start-technologies/releases/download/start-cli/v1.1.0/start-cli_x86_64-linux \
-o /tmp/start-cli
echo '70eff67b6e9a936acd8aaaf787b783819252ecedaa5c74d462e3b15ed4dd843a /tmp/start-cli' | sha256sum -c -
install -m 0755 /tmp/start-cli "$HOME/.local/bin/start-cli"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: npm ci
- run: npm run check && npm run build
- run: ./scripts/verify-release-inputs.sh
- run: ./scripts/audit-build-dependencies.sh
- name: Initialize StartOS workspace
run: cd .. && start-cli s9pk init-workspace
- name: Build package
run: make ${{ matrix.make_target }}
- name: Checksums
run: sha256sum '${{ matrix.artifact }}' > '${{ matrix.artifact }}.sha256'
- name: Generate SPDX SBOM
uses: anchore/sbom-action@v0.24.0
with:
path: '${{ matrix.artifact }}'
format: spdx-json
output-file: '${{ matrix.artifact }}.spdx.json'
- uses: actions/attest-build-provenance@v2
with:
subject-path: '${{ matrix.artifact }}'
- uses: actions/upload-artifact@v4
with:
name: '${{ matrix.artifact }}'
path: |
${{ matrix.artifact }}
${{ matrix.artifact }}.sha256
${{ matrix.artifact }}.spdx.json
npm-audit.json
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
javascript/
*.s9pk
*.s9pk.*
npm-audit.json
3 changes: 3 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Audited field-name false positives; no secret values are present.
b0b7295168dbeec7b097be3bf5319f3888353958:startos/main.ts:generic-api-key:120
b0b7295168dbeec7b097be3bf5319f3888353958:scripts/verify-release-inputs.sh:generic-api-key:27
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ boundary.
This repository is an early sideload beta. Build and install it only on a test
server, and back up the GridPool volume before upgrades.

Runtime images are pinned by immutable OCI digest. Release artifacts are built
for x86_64 and aarch64 by GitHub Actions and include SHA-256 checksums plus
provenance attestations.

## Build

```bash
Expand Down Expand Up @@ -58,3 +62,51 @@ worker labels use the package fallback address.
- GridPool identity/state, SV2 keys, adapter token, and proof spool are included
in StartOS backups.
- DATUM and raw Stratum V1 are intentionally not packaged.
- The appliance exports native SV2 TCP and authenticated GridPool UDP relay.
Bitcoin RPC/ZMQ, adapter APIs, SV2 monitoring, and the UI backend remain
private package interfaces.

## Backup, upgrade, and recovery

The StartOS `main` volume is the complete GridPool appliance backup unit. It
contains the payout setting, GridPool identity and consensus state, native-SV2
authority keys, adapter token, and durable proof spool. Bitcoin chain data is
owned by the Bitcoin dependency and is not duplicated.

Before every sideload upgrade:

1. Create a StartOS backup containing GridPool and verify that the backup is
listed on the configured target.
2. Record the GridPool node-ID fingerprint and installed package version.
3. Sideload the new package over the existing installation; do not uninstall
first.
4. Confirm the node ID, payout setting, Bitcoin authority, and SV2 health after
startup.

For disaster recovery, install the same or a compatible package version and use
StartOS **Restore From Backup**. A successful restore must preserve the node ID
and SV2 authority. Never resolve a state mismatch by deleting the `main` volume.

A normal uninstall deletes service data. Use a verified encrypted backup first;
`start-cli package uninstall --soft gridpool` is reserved for controlled package
lifecycle testing where preserving the volume is intentional.

## Release verification

```bash
npm ci
npm run check
npm run build
./scripts/verify-release-inputs.sh
./scripts/audit-build-dependencies.sh
```

Start SDK 2.0.9 currently bundles high-severity parser advisories in its ESLint
build-tool subtree. The audit script permits only those exact non-runtime paths
and verifies they do not enter the generated procedure bundle. Any runtime or
unrelated high/critical advisory fails CI.

StartOS cross-architecture packing cannot currently consume a multi-arch image
by index-digest reference. The manifest therefore uses commit-addressed `sha-*`
tags, while `release-images.json` locks their OCI index digests and the release
build verifies each tag still resolves to the expected digest before packing.
10 changes: 10 additions & 0 deletions release-images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"gridpool": {
"reference": "ghcr.io/gridlabs-science/boot-protocol:sha-9ac862a",
"indexDigest": "sha256:1a302f3d1a1de3df7b26a4cde6372bf31d453bd024ca697bd6f0a1a09c90b601"
},
"sv2": {
"reference": "ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-1151f92",
"indexDigest": "sha256:3de54e9c51953d2b5089ae90a25b9a0fa046c18c4ae0139b768b77a4cf0ed50e"
}
}
24 changes: 24 additions & 0 deletions scripts/audit-build-dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail

report="${1:-npm-audit.json}"
npm audit --omit=dev --json > "$report" || true

# Start SDK 2.0.9 bundles these parsers for its own build tooling. Permit only
# findings inside that subtree and verify they never enter packaged procedures.
jq -e '
[
.vulnerabilities
| to_entries[]
| select(.value.severity == "high" or .value.severity == "critical")
| .value.nodes[]
| select(startswith("node_modules/@start9labs/start-sdk/node_modules/") | not)
] | length == 0
' "$report" >/dev/null

if [[ -d javascript ]] && rg -q 'brace-expansion|js-yaml|YAMLException' javascript; then
echo "vulnerable Start SDK build-only parser entered the packaged procedure bundle" >&2
exit 1
fi

echo "Dependency audit contains only documented Start SDK 2.0.9 build-tool advisories."
34 changes: 34 additions & 0 deletions scripts/verify-release-inputs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail

manifest="startos/manifest/index.ts"
lock="release-images.json"

for component in gridpool sv2; do
reference="$(jq -r --arg component "$component" '.[$component].reference' "$lock")"
expected="$(jq -r --arg component "$component" '.[$component].indexDigest' "$lock")"
grep -Fq "$reference" "$manifest"
[[ "$reference" =~ :sha-[0-9a-f]{7,40}$ ]]
[[ "$expected" =~ ^sha256:[0-9a-f]{64}$ ]]
if command -v docker >/dev/null && docker buildx version >/dev/null 2>&1; then
actual="$(docker buildx imagetools inspect "$reference" --format '{{json .Manifest}}' | jq -r '.digest // .Digest')"
[[ "$actual" == "$expected" ]] || {
echo "$component image digest mismatch: expected $expected, got $actual" >&2
exit 1
}
inspection="$(docker buildx imagetools inspect "$reference")"
grep -q 'linux/amd64' <<<"$inspection"
grep -q 'linux/arm64' <<<"$inspection"
fi
done

! git ls-files '*.s9pk' '*.s9pk.sha256' | grep -q .
grep -q "ed25519_private_key.*x25519_private_key\|x25519_private_key.*ed25519_private_key" startos/main.ts || \
grep -q "\['ed25519_private_key', 'x25519_private_key'\]" startos/main.ts
grep -q "GridPool UDP Relay" startos/interfaces.ts
grep -q "preferredExternalPort: udpRelayPort" startos/interfaces.ts
! grep -Eq 'preferredExternalPort: (8332|28332|28333|34290|5000)' startos/interfaces.ts
grep -q '^[[:space:]]*bootConfigPath,$' startos/main.ts
grep -A4 '^[[:space:]]*bootConfigPath,$' startos/main.ts | grep -q "mode: 0o600"
grep -q "writeFile(tokenPath.*mode: 0o600" startos/main.ts
echo "release image tags resolve to locked OCI digests and package artifacts are untracked"
29 changes: 27 additions & 2 deletions startos/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sdk } from './sdk'
import { sv2Port, uiPort } from './utils'
import { sv2Port, udpRelayPort, uiPort } from './utils'

export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const uiOrigin = await sdk.MultiHost.of(effects, 'ui').bindPort(uiPort, {
Expand Down Expand Up @@ -35,5 +35,30 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
query: {},
})

return [await uiOrigin.export([ui]), await sv2Origin.export([sv2])]
const relayOrigin = await sdk.MultiHost.of(effects, 'gridpool-relay').bindPort(
udpRelayPort,
{
protocol: null,
addSsl: null,
preferredExternalPort: udpRelayPort,
secure: { ssl: false },
},
)
const relay = sdk.createInterface(effects, {
name: 'GridPool UDP Relay',
id: 'gridpool-relay',
description: 'Authenticated GridPool proof and chain-tip relay',
type: 'p2p',
masked: false,
schemeOverride: { ssl: null, noSsl: 'gridpool-udp' },
username: null,
path: '',
query: {},
})

return [
await uiOrigin.export([ui]),
await sv2Origin.export([sv2]),
await relayOrigin.export([relay]),
]
})
34 changes: 32 additions & 2 deletions startos/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from 'node:fs/promises'
import { settingsJson } from './fileModels/settings.json'
import { sdk } from './sdk'
import { bitcoinMount, sv2Port, uiPort } from './utils'
import { bitcoinMount, sv2Port, udpRelayPort, uiPort } from './utils'

const volumeRoot = '/media/startos/volumes/main'

Expand Down Expand Up @@ -109,6 +109,24 @@ export const main = sdk.setupMain(async ({ effects }) => {
throw new Error('Stored SV2 authority keypair is malformed')
}

const bootConfigPath = `${volumeRoot}/gridpool/boot_portal_config.json`
let persistedIdentity: Record<string, string> = {}
try {
const existing = JSON.parse(await readFile(bootConfigPath, 'utf8')) as Record<
string,
unknown
>
persistedIdentity = Object.fromEntries(
['ed25519_private_key', 'x25519_private_key']
.filter(
(key) => typeof existing[key] === 'string' && existing[key] !== '',
)
.map((key) => [key, existing[key] as string]),
)
} catch {
// First start has no identity yet. The node creates and persists it here.
}

const bootConfig = {
bitcoin_notification_mode: 'attached-node',
NotificationSource: 'BitcoinZmq',
Expand Down Expand Up @@ -137,9 +155,10 @@ export const main = sdk.setupMain(async ({ effects }) => {
local_adapter_token_file: '/data/shared/local-adapter.token',
local_sv2_api_url: 'http://127.0.0.1:34290/api/v1/global',
enable_admin_api: false,
...persistedIdentity,
}
await writeFile(
`${volumeRoot}/gridpool/boot_portal_config.json`,
bootConfigPath,
`${JSON.stringify(bootConfig, null, 2)}\n`,
{ mode: 0o600 },
)
Expand Down Expand Up @@ -288,6 +307,17 @@ min_interval = 5
},
requires: ['gridpool'],
})
.addHealthCheck('gridpool-udp', {
ready: {
display: 'GridPool UDP Relay',
fn: () =>
sdk.healthCheck.checkPortListening(effects, udpRelayPort, {
successMessage: 'GridPool UDP relay is listening',
errorMessage: 'GridPool UDP relay is not listening',
}),
},
requires: ['gridpool'],
})
.addDaemon('sv2', {
subcontainer: sv2Sub,
exec: {
Expand Down
2 changes: 1 addition & 1 deletion startos/manifest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const manifest = setupManifest({
sv2: {
source: {
dockerTag:
'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-36465d2',
'ghcr.io/gridlabs-science/gridpool-sv2-pool:sha-1151f92',
},
arch: ['x86_64', 'aarch64'],
},
Expand Down
1 change: 1 addition & 0 deletions startos/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const uiPort = 5000
export const sv2Port = 34265
export const udpRelayPort = 5001
export const bitcoinMount = '/mnt/bitcoin'
6 changes: 3 additions & 3 deletions startos/versions/current.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'

export const current = VersionInfo.of({
version: '0.1.0:15',
version: '0.1.0:19',
releaseNotes: {
en_US:
'Isolates and coalesces dashboard reads so the Web UI cannot exhaust public API rate limits.',
'Exports the authenticated UDP relay and adds release-candidate lifecycle and supply-chain checks.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})
}).satisfies('0.1.0:17')
Loading