|
|
|
|
@@ -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).
|
|
|
|
|
|