DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
167 lines
7.9 KiB
Bash
167 lines
7.9 KiB
Bash
#!/usr/bin/env bash
|
|
# deploy/single/update.sh — pull-and-restart update for a DChain single node.
|
|
#
|
|
# Modes
|
|
# ─────
|
|
# 1. RELEASE mode (preferred): DCHAIN_UPDATE_SOURCE_URL is set, node has
|
|
# /api/update-check — we trust that endpoint to tell us what's latest,
|
|
# then git-checkout the matching tag.
|
|
# 2. BRANCH mode (fallback): follow origin/main HEAD.
|
|
#
|
|
# Both modes share the same safety flow:
|
|
# - Fast-forward check (no auto-rewriting local history).
|
|
# - Smoke-test the new binary BEFORE killing the running container.
|
|
# - Pre-restart best-effort health probe.
|
|
# - Poll /api/netstats after restart, fail loud if unhealthy in 60s.
|
|
#
|
|
# Semver guard
|
|
# ────────────
|
|
# If UPDATE_ALLOW_MAJOR=false (default), the script refuses to cross a major
|
|
# version boundary (v1.x → v2.y). Operator must flip the env var or bump
|
|
# manually, avoiding surprise breaking changes from unattended restarts.
|
|
#
|
|
# Exit codes:
|
|
# 0 — up to date or successfully updated
|
|
# 1 — new container didn't become healthy
|
|
# 2 — smoke test of new image failed
|
|
# 3 — git state issue (fetch failed, not fast-forwardable, etc.)
|
|
# 4 — semver guard blocked the update
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_DIR="${REPO_DIR:-/opt/dchain}"
|
|
COMPOSE_FILE="${COMPOSE_FILE:-$REPO_DIR/deploy/single/docker-compose.yml}"
|
|
IMAGE_NAME="${IMAGE_NAME:-dchain-node-slim}"
|
|
CONTAINER="${CONTAINER:-dchain_node}"
|
|
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8080/api/netstats}"
|
|
UPDATE_CHECK_URL="${UPDATE_CHECK_URL:-http://127.0.0.1:8080/api/update-check}"
|
|
GIT_REMOTE="${GIT_REMOTE:-origin}"
|
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
|
UPDATE_ALLOW_MAJOR="${UPDATE_ALLOW_MAJOR:-false}"
|
|
|
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
|
|
die() { log "ERROR: $*"; exit "${2:-1}"; }
|
|
|
|
command -v docker >/dev/null || die "docker not on PATH" 3
|
|
command -v git >/dev/null || die "git not on PATH" 3
|
|
command -v curl >/dev/null || die "curl not on PATH" 3
|
|
|
|
cd "$REPO_DIR" || die "cannot cd to REPO_DIR=$REPO_DIR" 3
|
|
|
|
# ── Auth header for /api/update-check + /api/netstats on private nodes ────
|
|
auth_args=()
|
|
[[ -n "${DCHAIN_API_TOKEN:-}" ]] && auth_args=(-H "Authorization: Bearer ${DCHAIN_API_TOKEN}")
|
|
|
|
# ── 1. Discover target version ────────────────────────────────────────────
|
|
target_tag=""
|
|
target_commit=""
|
|
|
|
if [[ -n "${DCHAIN_UPDATE_SOURCE_URL:-}" ]]; then
|
|
log "querying release source via $UPDATE_CHECK_URL"
|
|
check_json=$(curl -fsS -m 10 "${auth_args[@]}" "$UPDATE_CHECK_URL" 2>/dev/null || echo "")
|
|
if [[ -n "$check_json" ]]; then
|
|
# Minimal grep-based JSON extraction — avoids adding a jq dependency.
|
|
# The shape is stable: we defined it in api_update_check.go.
|
|
target_tag=$(printf '%s' "$check_json" | grep -o '"tag":"[^"]*"' | head -1 | sed 's/"tag":"\(.*\)"/\1/')
|
|
update_available=$(printf '%s' "$check_json" | grep -o '"update_available":\(true\|false\)' | head -1 | cut -d: -f2)
|
|
if [[ "$update_available" != "true" ]]; then
|
|
log "up to date according to release source ($target_tag) — nothing to do"
|
|
exit 0
|
|
fi
|
|
log "release source reports new tag: $target_tag"
|
|
else
|
|
log "release source query failed — falling back to git branch mode"
|
|
fi
|
|
fi
|
|
|
|
# ── 2. Fetch git ──────────────────────────────────────────────────────────
|
|
log "fetching $GIT_REMOTE"
|
|
git fetch --quiet --tags "$GIT_REMOTE" "$GIT_BRANCH" || die "git fetch failed" 3
|
|
|
|
local_sha=$(git rev-parse HEAD)
|
|
if [[ -n "$target_tag" ]]; then
|
|
# Release mode: target is the tag we just read from the API.
|
|
if ! git rev-parse --verify "refs/tags/$target_tag" >/dev/null 2>&1; then
|
|
die "release tag $target_tag unknown to local git — fetch may have missed it" 3
|
|
fi
|
|
target_commit=$(git rev-parse "refs/tags/$target_tag^{commit}")
|
|
else
|
|
# Branch mode: target is remote branch HEAD.
|
|
target_commit=$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")
|
|
target_tag=$(git describe --tags --abbrev=0 "$target_commit" 2>/dev/null || echo "$target_commit")
|
|
fi
|
|
|
|
if [[ "$local_sha" == "$target_commit" ]]; then
|
|
log "up to date at $local_sha — nothing to do"
|
|
exit 0
|
|
fi
|
|
log "updating $local_sha → $target_commit ($target_tag)"
|
|
|
|
# ── 3. Semver guard ───────────────────────────────────────────────────────
|
|
# Extract major components from vX.Y.Z tags; refuse to cross boundary unless
|
|
# operator opts in. Treats non-semver tags (e.g. raw SHA in branch mode) as
|
|
# unversioned — guard is a no-op there.
|
|
current_tag=$(cat "$REPO_DIR/.last-update" 2>/dev/null || echo "")
|
|
if [[ -z "$current_tag" ]]; then
|
|
current_tag=$(git describe --tags --abbrev=0 "$local_sha" 2>/dev/null || echo "")
|
|
fi
|
|
current_major=$(printf '%s' "$current_tag" | sed -nE 's/^v([0-9]+)\..*/\1/p')
|
|
target_major=$(printf '%s' "$target_tag" | sed -nE 's/^v([0-9]+)\..*/\1/p')
|
|
if [[ -n "$current_major" && -n "$target_major" && "$current_major" != "$target_major" ]]; then
|
|
if [[ "$UPDATE_ALLOW_MAJOR" != "true" ]]; then
|
|
die "major version jump $current_tag → $target_tag blocked — set UPDATE_ALLOW_MAJOR=true to override" 4
|
|
fi
|
|
log "semver guard: accepting major jump $current_tag → $target_tag (UPDATE_ALLOW_MAJOR=true)"
|
|
fi
|
|
|
|
# ── 4. Fast-forward / checkout ────────────────────────────────────────────
|
|
if [[ -n "$target_tag" ]] && git rev-parse --verify "refs/tags/$target_tag" >/dev/null 2>&1; then
|
|
# Release mode: check out the tag in detached HEAD.
|
|
git checkout --quiet "$target_tag" || die "checkout $target_tag failed" 3
|
|
else
|
|
git merge --ff-only "$GIT_REMOTE/$GIT_BRANCH" || die "cannot fast-forward — manual merge required" 3
|
|
fi
|
|
|
|
# ── 5. Build image with version metadata ──────────────────────────────────
|
|
version_tag="$target_tag"
|
|
version_commit="$target_commit"
|
|
version_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
version_dirty=$(git diff --quiet HEAD -- 2>/dev/null && echo false || echo true)
|
|
|
|
log "building image $IMAGE_NAME:$version_commit ($version_tag)"
|
|
docker build --quiet \
|
|
--build-arg "VERSION_TAG=$version_tag" \
|
|
--build-arg "VERSION_COMMIT=$version_commit" \
|
|
--build-arg "VERSION_DATE=$version_date" \
|
|
--build-arg "VERSION_DIRTY=$version_dirty" \
|
|
-t "$IMAGE_NAME:$version_commit" \
|
|
-t "$IMAGE_NAME:latest" \
|
|
-f deploy/prod/Dockerfile.slim . \
|
|
|| die "docker build failed" 2
|
|
|
|
# ── 6. Smoke-test the new binary ──────────────────────────────────────────
|
|
log "smoke-testing new image"
|
|
smoke=$(docker run --rm --entrypoint /usr/local/bin/node "$IMAGE_NAME:$version_commit" --version 2>&1) \
|
|
|| die "new image --version failed: $smoke" 2
|
|
log "smoke ok: $smoke"
|
|
|
|
# ── 7. Recreate the container ─────────────────────────────────────────────
|
|
log "recreating container $CONTAINER"
|
|
docker compose -f "$COMPOSE_FILE" up -d --force-recreate node \
|
|
|| die "docker compose up failed" 1
|
|
|
|
# ── 8. Wait for health ────────────────────────────────────────────────────
|
|
log "waiting for health at $HEALTH_URL"
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS -m 3 "${auth_args[@]}" "$HEALTH_URL" >/dev/null 2>&1; then
|
|
log "node healthy after $((i*2))s — update done ($version_tag @ ${version_commit:0:8})"
|
|
printf '%s\n' "$version_tag" > .last-update
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
log "new container did not become healthy in 60s — dumping logs"
|
|
docker logs --tail 80 "$CONTAINER" || true
|
|
exit 1
|