diff --git a/scripts/sign-snapshot.sh b/scripts/sign-snapshot.sh new file mode 100755 index 0000000..ba005fe --- /dev/null +++ b/scripts/sign-snapshot.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# ============================================================================ +# Triangles UTXO Snapshot Signer +# ============================================================================ +# Generates a UTXO snapshot from the current node, signs its provenance +# message with the wallet's signing address, and writes the signed manifest. +# +# Usage: +# ./sign-snapshot.sh [snapshot-name] +# +# Default snapshot name: tri-utxo-snapshot-.utx +# Output (in this dir): +# - the UTXO snapshot binary +# .sig - base64 signature +# .msg - signed message (human-readable provenance) +# .manifest.json - signed manifest (drop into bootstrap dir) +# .pubkey - signing address +# +# Requirements: +# - trianglesd running with RPC enabled +# - wallet unlocked (or passphrase set in triangles.conf) +# - jq installed (apt: jq / brew: jq) +# +# Verification: +# ./sign-snapshot.sh verify +# OR via RPC: +# verifymessage +# ============================================================================ + +set -euo pipefail + +# ----- Config (override via env) ----- +RPC_USER="${RPC_USER:-trianglesrpc}" +RPC_PASS="${RPC_PASS:-2KVK2FvLZBW9Hxv4a2Uj3dMRDAXdh4ei6S5tdZ3z2Mme}" +RPC_HOST="${RPC_HOST:-127.0.0.1}" +RPC_PORT="${RPC_PORT:-19112}" +SIGN_ACCOUNT="${SIGN_ACCOUNT:-}" # blank = use default account +NHEADERS="${NHEADERS:-2000}" +SNAP_DIR="${SNAP_DIR:-.}" + +# ----- Helpers ----- +rpc() { + local method="$1"; shift + local params="$1"; shift || true + curl -s --user "${RPC_USER}:${RPC_PASS}" \ + -X POST -H 'Content-Type: application/json' \ + --data "{\"jsonrpc\":\"1.0\",\"method\":\"${method}\",\"params\":${params}}" \ + "http://${RPC_HOST}:${RPC_PORT}/" +} + +rpc_field() { + local method="$1"; shift + local params="$1"; shift || true + local field="$1"; shift + rpc "$method" "$params" | jq -r ".result.${field} // empty" +} + +sha256_file() { sha256sum "$1" | awk '{print $1}'; } + +# ----- Verify mode ----- +if [[ "${1:-}" == "verify" ]]; then + MANIFEST="${2:?usage: $0 verify }" + SNAP="${3:?usage: $0 verify }" + ADDR=$(jq -r '.signing_address' "$MANIFEST") + SIG=$(jq -r '.signature' "$MANIFEST") + MSG=$(jq -r '.message' "$MANIFEST") + EXPECTED_SHA=$(jq -r '.snapshot_sha256' "$MANIFEST") + + echo "==> Verifying snapshot provenance..." + echo " Address: $ADDR" + echo " Message: $MSG" + + ACTUAL_SHA=$(sha256_file "$SNAP") + if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then + echo "FAIL: snapshot sha256 mismatch" + echo " expected: $EXPECTED_SHA" + echo " actual: $ACTUAL_SHA" + exit 1 + fi + echo "OK: sha256 matches" + + PARAMS=$(jq -nc --arg a "$ADDR" --arg s "$SIG" --arg m "$MSG" \ + '[$a, $s, $m]') + RESULT=$(rpc verifymessage "$PARAMS" | jq -r '.result') + if [[ "$RESULT" == "true" ]]; then + echo "OK: signature valid — snapshot was signed by $ADDR" + exit 0 + else + echo "FAIL: signature does not verify" + exit 1 + fi +fi + +# ----- Generate + sign ----- +SNAP_NAME="${1:-tri-utxo-snapshot-$(date -u +%Y%m%dT%H%M%SZ).utx}" +SNAP_PATH="${SNAP_DIR}/${SNAP_NAME}" + +echo "==> Step 1/5: querying chain state..." +HEIGHT=$(rpc_field getblockcount '[]' '' || echo "") +if [[ -z "$HEIGHT" ]]; then + rpc_field getblockcount '[]' '' # re-run for error visibility + echo "FAIL: RPC getblockcount failed"; exit 1 +fi +HEIGHT=$(rpc getblockcount '[]' | jq -r '.result') +BLOCKHASH=$(rpc getbestblockhash '[]' | jq -r '.result') +echo " height: $HEIGHT" +echo " blockhash:$BLOCKHASH" + +echo "==> Step 2/5: selecting signing address..." +if [[ -n "$SIGN_ACCOUNT" ]]; then + PARAMS=$(jq -nc --arg a "$SIGN_ACCOUNT" '[$a]') +else + PARAMS='[""]' +fi +ADDR=$(rpc getaccountaddress "$PARAMS" | jq -r '.result') +echo " signer: $ADDR" + +echo "==> Step 3/5: dumping UTXO snapshot..." +PARAMS=$(jq -nc --arg f "$SNAP_PATH" --argjson n "$NHEADERS" '[$f, $n]') +DUMP_RESULT=$(rpc dumputxoset "$PARAMS") +echo "$DUMP_RESULT" | jq -r '.result // .error.message // .' +SIZE=$(echo "$DUMP_RESULT" | jq -r '.result.file_size // empty') +if [[ -z "$SIZE" ]]; then + echo "FAIL: dumputxoset failed"; exit 1 +fi +echo " size: $SIZE bytes" + +echo "==> Step 4/5: signing provenance message..." +SHA=$(sha256_file "$SNAP_PATH") +MSG="Triangles UTXO Snapshot $(date -u +%Y-%m-%d): height=$HEIGHT hash=$BLOCKHASH sha256=$SHA" +echo " message: $MSG" +PARAMS=$(jq -nc --arg a "$ADDR" --arg m "$MSG" '[$a, $m]') +SIG=$(rpc signmessage "$PARAMS" | jq -r '.result') +echo " sig: $SIG" + +echo "==> Step 5/5: writing manifest + sidecars..." +MANIFEST_PATH="${SNAP_PATH}.manifest.json" +jq -n \ + --arg name "$SNAP_NAME" \ + --arg height "$HEIGHT" \ + --arg hash "$BLOCKHASH" \ + --arg sha "$SHA" \ + --arg size "$SIZE" \ + --arg msg "$MSG" \ + --arg sig "$SIG" \ + --arg addr "$ADDR" \ + --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --arg ver "$(rpc getnetworkinfo '[]' | jq -r '.result.version // "unknown"')" \ + '{ + schema: "triangles-utxo-snapshot-signed/v1", + name: $name, + generated_utc: $ts, + daemon_version: $ver, + chain_tip: { height: ($height | tonumber), blockhash: $hash }, + snapshot_sha256: $sha, + snapshot_bytes: ($size | tonumber), + signing_address: $addr, + message: $msg, + signature: $sig + }' > "$MANIFEST_PATH" + +# Sidecar files for easy reading +echo "$ADDR" > "${SNAP_PATH}.pubkey" +echo "$MSG" > "${SNAP_PATH}.msg" +echo "$SIG" > "${SNAP_PATH}.sig" + +echo "" +echo "============================================================" +echo "Snapshot signed." +echo " snapshot: $SNAP_PATH" +echo " signature: ${SNAP_PATH}.sig" +echo " manifest: $MANIFEST_PATH" +echo " signer: $ADDR" +echo " sha256: $SHA" +echo "============================================================" +echo "" +echo "To verify on any node:" +echo " verifymessage $ADDR \\" +echo " '$SIG' \\" +echo " '$MSG'" +echo "" +echo "Or run: $0 verify $MANIFEST_PATH $SNAP_PATH" \ No newline at end of file