feat(infra): our NEAR nodes public by shard, s<id>.near.blackbeard.observer

near.blackbeard.observer reaches only frankie, and a NEAR node refuses any
account on a shard it does not track. The wallet's NEAR Intents account
(blackbeard/wallet#71) needs reads on every shard its route touches, so
each shard one of our nodes tracks now has its own name on oolon:
s4/s8 to gargravarr, s7/s13 to frankie, s10/s12 to quadbrat. One vhost
and one certificate lineage cover them all, the shard-to-node map sits
in blackbeard-nodes.conf with a request ceiling per node, and
infra-setup.sh --role nodes provisions them (NEAR_SHARDS).

Already applied: DNS, the certificate (to 2027-01-07) and the vhost, all
answering status. Every name answers for an account on its shard except
s13. That needs a node for wrap.near: adding it to frankie's tracked
accounts stalled the node, as CLAUDE.md now records, and was reverted.

Refs blackbeard/wallet#73

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT
This commit is contained in:
2026-10-09 12:41:56 +03:00
co-authored by Claude Opus 5.5
parent 0378d0efcb
commit 36bea6bb7f
4 changed files with 173 additions and 11 deletions
+34 -11
View File
@@ -58,6 +58,12 @@ SITE_INDIRECTION="${SITE_INDIRECTION:-bl.thgttg.com}"
# `near` is not a wallet chain: it is our NEAR RPC node (blackbeard/observer#29),
# published the same way and throttled harder in its own vhost.
NODE_CHAINS="${NODE_CHAINS:-quantus planck near}"
# The NEAR shards our nodes track, published as `s<id>.near.blackbeard.observer`
# (blackbeard/wallet#73): one vhost (near-shards.blackbeard.observer.conf), one
# certificate lineage naming them all, and the shard-to-node map in
# blackbeard-nodes.conf. All three change together when NEAR reshards.
NEAR_SHARDS="${NEAR_SHARDS:-4 7 8 10 12 13}"
NEAR_SHARDS_LINEAGE="near-shards.${PUBLIC_NAME}"
CERT_EMAIL="${CERT_EMAIL:-ops@blackbeard.observer}"
PUBKEY=""
@@ -475,24 +481,29 @@ sys.exit(0 if d["success"] else 1)'
REMOTE
}
# node_cert LINEAGE [NAME...]: one certificate lineage for the names given, or
# for LINEAGE alone. --expand lets a lineage gain a name (a new shard) in place.
node_cert() {
local name="$1"
local lineage="$1"; shift
local names="${*:-$lineage}"
# Same recipe as role_cert; the deploy hook and timer are host-wide and
# already handled there.
ssh "$EDGE_HOST" "NAME='$name' CERT_EMAIL='$CERT_EMAIL' bash -euo pipefail" <<'REMOTE'
if sudo test -d "/etc/letsencrypt/live/${NAME}"; then
echo "certificate lineage ${NAME} already exists"
ssh "$EDGE_HOST" "LINEAGE='$lineage' NAMES='$names' CERT_EMAIL='$CERT_EMAIL' bash -euo pipefail" <<'REMOTE'
if sudo test -d "/etc/letsencrypt/live/${LINEAGE}"; then
echo "certificate lineage ${LINEAGE} already exists"
fi
domains=()
for n in $NAMES; do domains+=(-d "$n"); done
sudo certbot certonly \
-m "$CERT_EMAIL" --agree-tos --no-eff-email --noninteractive \
--cert-name "$NAME" \
--cert-name "$LINEAGE" \
--key-type ecdsa \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.certbot-internal \
--dns-cloudflare-propagation-seconds 60 \
--keep-until-expiring \
-d "$NAME"
sudo test -f "/etc/letsencrypt/live/${NAME}/fullchain.pem" \
--keep-until-expiring --expand \
"${domains[@]}"
sudo test -f "/etc/letsencrypt/live/${LINEAGE}/fullchain.pem" \
|| { echo "certbot reported success but no fullchain.pem exists" >&2; exit 1; }
REMOTE
}
@@ -513,14 +524,26 @@ role_nodes() {
node_cert "$name"
done
local shard shard_names=()
for shard in $NEAR_SHARDS; do shard_names+=("s${shard}.near.${PUBLIC_NAME}"); done
[ -f "$REPO_ROOT/asset/nginx/${NEAR_SHARDS_LINEAGE}.conf" ] \
|| { echo "no vhost asset/nginx/${NEAR_SHARDS_LINEAGE}.conf" >&2; return 1; }
for name in "${shard_names[@]}"; do
info "$EDGE_HOST: public DNS for $name"
node_dns "$name"
done
info "$EDGE_HOST: Let's Encrypt certificate $NEAR_SHARDS_LINEAGE for ${shard_names[*]}"
node_cert "$NEAR_SHARDS_LINEAGE" "${shard_names[@]}"
info "$EDGE_HOST: node upstreams, throttle zones and vhosts"
rsync -a --rsync-path 'sudo rsync' \
"$REPO_ROOT/asset/nginx/blackbeard-nodes.conf" \
"$EDGE_HOST:/etc/nginx/conf.d/blackbeard-nodes.conf"
local files=()
for name in "${names[@]}"; do files+=("$REPO_ROOT/asset/nginx/${name}.conf"); done
files+=("$REPO_ROOT/asset/nginx/${NEAR_SHARDS_LINEAGE}.conf")
rsync -a --rsync-path 'sudo rsync' "${files[@]}" "$EDGE_HOST:/etc/nginx/sites-available/"
ssh "$EDGE_HOST" "NAMES='${names[*]}' bash -euo pipefail" <<'REMOTE'
ssh "$EDGE_HOST" "NAMES='${names[*]} ${NEAR_SHARDS_LINEAGE}' bash -euo pipefail" <<'REMOTE'
# Enable, test, and only then reload. A vhost that fails the test is
# disabled again before leaving, so a typo here never blocks the next
# reload of every other site on this proxy.
@@ -539,12 +562,12 @@ role_nodes() {
INNER
REMOTE
for name in "${names[@]}"; do
for name in "${names[@]}" "${shard_names[@]}"; do
info "$name answers"
# Each node is asked something its own RPC understands: NEAR has no
# system_chain, and a Substrate node has no status method.
local method=system_chain params='[]'
case "$name" in near.*) method=status params='{}' ;; esac
case "$name" in near.*|s*.near.*) method=status params='{}' ;; esac
curl -sS --max-time 15 -H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" \
"https://${name}" | head -c 400 && echo