deploy: node from a chain spec file with genesis assertion, bootnodes as reserved peers, raised peer limits #24

Merged
grenade merged 1 commits from launch/chainspec-deploy into main 2026-09-09 05:05:35 +00:00
4 changed files with 118 additions and 3 deletions

View File

@@ -38,6 +38,15 @@ on:
node_version:
description: quantus-node version (overrides the pin)
required: false
chainspec_url:
description: "chain spec JSON to run from (overrides CHAINSPEC_URL); empty = the built-in CHAIN name"
required: false
expected_genesis:
description: "0x genesis hash the started node must report (overrides EXPECTED_GENESIS); required with a chain spec"
required: false
bootnodes:
description: "space-separated multiaddrs to dial and hold as reserved peers (overrides BOOTNODES)"
required: false
concurrency: # never half-apply two deploys at once
group: deploy
@@ -52,6 +61,21 @@ env:
# Pinned; bump deliberately to upgrade. An unattended upgrade of a validator
# is how you find out at 3am that a release changed a consensus rule.
NODE_VERSION: "0.11.1"
# Mainnet launch (2026-09-10 09:09 UTC): the chain spec is published as a
# file before a binary embeds it, so the node can run from a JSON at
# /etc/quantus-node/chainspec.json instead of a built-in name. When set, the
# spec's `id` (not CHAIN above) names <base-path>/chains/<id>/, which is
# where quantus/miner's deploy reads the miner credentials from — pass it
# there as the `chain` input. EXPECTED_GENESIS is asserted against the
# started node's block-0 hash, which is the whole defence against running
# a look-alike spec. BOOTNODES are dialled and held as reserved peers:
# launch is a propagation race, and a direct link to the foundation's nodes
# is the one latency knob we have (quantus/chain#1). Dispatch inputs
# override each of these for the cutover; commit the values afterwards so
# push-triggered deploys keep them.
CHAINSPEC_URL: ""
EXPECTED_GENESIS: ""
BOOTNODES: ""
# The miner is deployed by quantus/miner's own workflow, following its main
# branch (quantus/miner#8, lair/quantus#4). This repo no longer ships it;
# SCRAPE_MINERS below must still agree with that repo's deploy matrix.
@@ -87,6 +111,10 @@ jobs:
runs-on: infra
outputs:
node_version: ${{ steps.v.outputs.node }}
chainspec_url: ${{ steps.v.outputs.chainspec_url }}
expected_genesis: ${{ steps.v.outputs.expected_genesis }}
bootnodes: ${{ steps.v.outputs.bootnodes }}
chain_id: ${{ steps.cs.outputs.chain_id }}
steps:
- id: v
run: |
@@ -94,6 +122,46 @@ jobs:
n="${{ github.event.inputs.node_version || env.NODE_VERSION }}"
echo "node=${n}" >> "$GITHUB_OUTPUT"
echo "quantus-node ${n}"
cs="${{ github.event.inputs.chainspec_url || env.CHAINSPEC_URL }}"
eg="${{ github.event.inputs.expected_genesis || env.EXPECTED_GENESIS }}"
bn="${{ github.event.inputs.bootnodes || env.BOOTNODES }}"
if [ -n "$cs" ] && [ -z "$eg" ]; then
echo "a chain spec needs expected_genesis: without it a look-alike spec would pass validation" >&2
exit 1
fi
case "$eg" in ""|0x[0-9a-f]*) ;; *) echo "expected_genesis must be a 0x hex hash" >&2; exit 1 ;; esac
for b in $bn; do
case "$b" in /*/p2p/*) ;; *) echo "bootnode '$b' is not a multiaddr ending in /p2p/<peer id>" >&2; exit 1 ;; esac
done
echo "chainspec_url=${cs}" >> "$GITHUB_OUTPUT"
echo "expected_genesis=${eg}" >> "$GITHUB_OUTPUT"
echo "bootnodes=${bn}" >> "$GITHUB_OUTPUT"
echo "chain spec: ${cs:-built-in ${{ env.CHAIN }}}; expected genesis: ${eg:-not asserted}; bootnodes: ${bn:-none}"
- name: download chain spec
id: cs
run: |
set -euo pipefail
cs="${{ steps.v.outputs.chainspec_url }}"
if [ -z "$cs" ]; then
echo "chain_id=${{ env.CHAIN }}" >> "$GITHUB_OUTPUT"
exit 0
fi
mkdir -p _bin
curl -fSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 15 \
-o _bin/chainspec.json "$cs"
# The spec must parse, and its id is the directory the node keeps
# this chain's state and miner credentials under.
id=$(python3 -c "
import json, sys
d = json.load(open('_bin/chainspec.json'))
for k in ('id', 'name', 'chainType'):
assert k in d, f'chain spec has no {k}'
print(f\"name={d['name']} id={d['id']} type={d['chainType']} bootNodes={len(d.get('bootNodes') or [])} telemetry={d.get('telemetryEndpoints')}\", file=sys.stderr)
print(d['id'])")
echo "chain_id=${id}" >> "$GITHUB_OUTPUT"
sha256sum _bin/chainspec.json
{ echo "chain spec: $cs"; echo "id: $id"; echo "sha256: $(sha256sum _bin/chainspec.json | cut -d' ' -f1)"; } >> "$GITHUB_STEP_SUMMARY"
- name: download quantus-node
run: |
@@ -221,7 +289,8 @@ jobs:
env:
QUANTUS_INNER_HASH: ${{ matrix.inner_hash }}
QUANTUS_PUBLIC_ADDR: ${{ matrix.public_addr }}
QUANTUS_CHAIN: ${{ env.CHAIN }}
QUANTUS_CHAIN: ${{ needs.fetch.outputs.chainspec_url != '' && '/etc/quantus-node/chainspec.json' || env.CHAIN }}
QUANTUS_BOOTNODES: ${{ needs.fetch.outputs.bootnodes }}
QUANTUS_RPC_EXPOSE: ${{ matrix.rpc_expose }}
QUANTUS_TELEMETRY_URL: ${{ env.TELEMETRY_URL }}
run: |
@@ -248,6 +317,10 @@ jobs:
out = out.replace("{{QUANTUS_PUBLIC_ADDR}}", os.environ.get("QUANTUS_PUBLIC_ADDR", ""))
out = out.replace("{{QUANTUS_CHAIN}}", os.environ["QUANTUS_CHAIN"])
out = out.replace("{{QUANTUS_RPC_EXPOSE}}", os.environ.get("QUANTUS_RPC_EXPOSE", ""))
# Each bootnode is dialled AND held as a reserved peer (not
# --reserved-only: the rest of the peer set still fills normally).
flags = " ".join(f"--bootnodes {b} --reserved-nodes {b}" for b in os.environ.get("QUANTUS_BOOTNODES", "").split())
out = out.replace("{{QUANTUS_BOOTNODES}}", flags)
pathlib.Path("node.env").write_text(out)
a = pathlib.Path("asset/config/arena.env.tmpl").read_text()
a = a.replace("{{QUANTUS_INNER_HASH}}", os.environ["QUANTUS_INNER_HASH"])
@@ -301,6 +374,13 @@ jobs:
gitea_ci@"$HOST":/etc/systemd/system/quantus-node.service
push --chown=root:quantus-node --chmod=F0640 node.env \
gitea_ci@"$HOST":/etc/quantus-node/node.env
# 3a. Chain spec file, when running from one. Left in place otherwise:
# node.env decides what --chain points at, and an unreferenced
# file is harmless while deleting it would need one more grant.
if [ -s _bin/chainspec.json ]; then
push --chown=root:quantus-node --chmod=F0640 _bin/chainspec.json \
gitea_ci@"$HOST":/etc/quantus-node/chainspec.json
fi
pushfw --mkpath --chmod=F0644 asset/firewalld/quantus-node.xml \
gitea_ci@"$HOST":/etc/firewalld/services/quantus-node.xml
pushfw --mkpath --chmod=F0644 asset/firewalld/quantus-node-miner.xml \
@@ -517,6 +597,32 @@ jobs:
fail=1
fi
echo "--- chain ---"
# Which chain the node actually started, from its own RPC on loopback.
# The genesis hash is the identity of the chain; asserting it is what
# stops a look-alike spec (or a stale built-in name) from mining the
# wrong network unnoticed.
rpc() { run "curl -sS -m 10 -H content-type:application/json -d '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":$2}' http://127.0.0.1:9944" \
| python3 -c 'import sys, json; print(json.load(sys.stdin).get("result", ""))'; }
chain_name=$(rpc system_chain '[]' || true)
genesis=$(rpc chain_getBlockHash '[0]' || true)
echo " chain: ${chain_name:-?} genesis: ${genesis:-?} id: ${{ needs.fetch.outputs.chain_id }}"
if [ -z "$genesis" ]; then
echo " FAIL node RPC did not answer chain_getBlockHash" >&2; fail=1
elif [ -n "${{ needs.fetch.outputs.expected_genesis }}" ] && [ "$genesis" != "${{ needs.fetch.outputs.expected_genesis }}" ]; then
echo " FAIL genesis ${genesis} is not the expected ${{ needs.fetch.outputs.expected_genesis }} — WRONG CHAIN" >&2; fail=1
elif [ -n "${{ needs.fetch.outputs.expected_genesis }}" ]; then
echo " genesis matches expected"
fi
# The miner credential directory follows the spec id; quantus/miner's
# deploy must be pointed at exactly this name.
if run "sudo cat /var/lib/quantus-node/chains/${{ needs.fetch.outputs.chain_id }}/miner-auth-token" | grep -q .; then
echo " miner credentials present under chains/${{ needs.fetch.outputs.chain_id }}/ (use chain=${{ needs.fetch.outputs.chain_id }} on the miner deploy)"
else
echo " FAIL no miner credentials under chains/${{ needs.fetch.outputs.chain_id }}/ — the chain id the miner deploy needs is not this" >&2; fail=1
fi
{ echo "chain: ${chain_name:-?}"; echo "genesis: ${genesis:-?}"; echo "chain id for the miner deploy: ${{ needs.fetch.outputs.chain_id }}"; } >> "$GITHUB_STEP_SUMMARY"
echo "--- listeners ---"
# P2P must be listening; the miner link must be listening or the node
# is authoring nothing (--miner-listen-port disables local mining).

View File

@@ -21,6 +21,11 @@ QUANTUS_PUBLIC_ADDR={{QUANTUS_PUBLIC_ADDR}}
# (<base-path>/chains/<chain>/) follows from the same value.
QUANTUS_CHAIN={{QUANTUS_CHAIN}}
# Bootnodes, rendered as complete flags: each address is dialled (--bootnodes)
# and held as a reserved peer (--reserved-nodes) so the connection to the
# foundation's nodes is never evicted. Empty = the spec's own bootNodes only.
QUANTUS_BOOTNODES={{QUANTUS_BOOTNODES}}
# JSON-RPC exposure. Empty = loopback only. A complete flag string opens it:
# --rpc-external --rpc-methods safe --rpc-cors all --rpc-rate-limit 300 \
# --rpc-rate-limit-whitelisted-ips 127.0.0.1/32

View File

@@ -1,4 +1,4 @@
# Quantus Planck node, validator role, external-miner mode.
# Quantus node, validator role, external-miner mode.
#
# Hardened per ~/git/architecture/generic.md §8. Two knobs are deliberately
# relaxed; both are load-bearing, do not "tidy" them back:
@@ -39,7 +39,7 @@
# gets packets in but never tells anyone to send them.
[Unit]
Description=Quantus node (Planck, validator, external-miner mode)
Description=Quantus node (validator, external-miner mode)
Documentation=https://github.com/Quantus-Network/chain
After=network-online.target
Wants=network-online.target
@@ -62,7 +62,10 @@ ExecStart=/usr/local/bin/quantus-node \
--miner-listen-port 9833 \
--port 30333 \
$QUANTUS_PUBLIC_ADDR \
$QUANTUS_BOOTNODES \
$QUANTUS_RPC_EXPOSE \
--out-peers 32 \
--in-peers 64 \
--prometheus-port 9615 \
--prometheus-external \
--max-blocks-per-request 64 \

View File

@@ -162,6 +162,7 @@ gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/quantus-node
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/quantus-node.service
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/quantus-node.conf
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-node/node.env
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-node/chainspec.json
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-node.xml
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-node-miner.xml
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-node-metrics.xml