From e15de97be3545374351547f028f81a8e24e42ecd Mon Sep 17 00:00:00 2001 From: hermes Date: Thu, 18 Jun 2026 02:34:51 -0700 Subject: [PATCH 1/3] utxosnapshot: gate requireCheckpoint on trust source Local file snapshots (init.cpp) skip the known-checkpoint gate; P2P-delivered snapshots (bootstrap.cpp) keep it. Rationale: the checkpoint gate exists to prevent malicious peers from injecting fake UTXO sets. Local file loads come from operator-trusted sources (filesystem access already grants equal power), so the gate is unnecessary friction. --- src/bootstrap.cpp | 7 +++++-- src/init.cpp | 7 ++++++- src/utxosnapshot.cpp | 16 +++++++++++++--- src/utxosnapshot.h | 6 +++++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 0c7bab5..188fc31 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -792,8 +792,11 @@ bool DownloadUtxoSnapshot(const std::string& host, printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n"); - // Load the snapshot into a fresh active chain DB - if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) { + // Load the snapshot into a fresh active chain DB. P2P-delivered + // snapshots keep the checkpoint gate on (requireCheckpoint=true) — + // the manifest height+hash already passed IsKnownCheckpoint above, + // and we re-check here as defense in depth. + if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/true)) { fs::remove(tmpPath); return false; } diff --git a/src/init.cpp b/src/init.cpp index 83077f5..2ad9e65 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1025,8 +1025,13 @@ bool AppInit2() printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n"); uiInterface.InitMessage(_("Loading UTXO snapshot...")); + // Local file load: skip the checkpoint gate. The operator has + // filesystem access, so the trust model is already equivalent + // to direct chain state modification — a malicious local file + // is no worse than a malicious chain DB. P2P-delivered + // snapshots (SnapshotNet) keep the checkpoint gate on. std::string strError; - if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) { + if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) { printf("UTXO snapshot loaded successfully.\n"); } else { printf("UTXO snapshot load failed: %s\n", strError.c_str()); diff --git a/src/utxosnapshot.cpp b/src/utxosnapshot.cpp index f931361..5f1cada 100644 --- a/src/utxosnapshot.cpp +++ b/src/utxosnapshot.cpp @@ -214,11 +214,20 @@ static bool SnapAddressFromScript(const CScript& script, int& nType, uint160& ha // --------------------------------------------------------------------------- // LoadSnapshot - load a UTXO snapshot into a fresh LevelDB +// +// `requireCheckpoint` controls whether the snapshot's tip block must be a +// known checkpoint. This gate exists to prevent malicious P2P peers from +// tricking the daemon into accepting a fake UTXO set at an arbitrary +// height on an alternate chain. Local file loads (operator already has +// filesystem access, so the trust model is the same as editing the chain +// state directly) skip the gate via requireCheckpoint=false. P2P-delivered +// snapshots (SnapshotNet) keep the gate on. // --------------------------------------------------------------------------- bool LoadSnapshot(const fs::path& snapshotPath, const fs::path& /*dataDir — unused; resolved per-backend via GetChainDataDir()*/, - std::string& strError) + std::string& strError, + bool requireCheckpoint) { FILE* file = fopen(snapshotPath.string().c_str(), "rb"); if (!file) { @@ -274,8 +283,9 @@ bool LoadSnapshot(const fs::path& snapshotPath, return false; } - // Verify snapshot block is a known checkpoint - if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) { + // Verify snapshot block is a known checkpoint (only for P2P-delivered + // snapshots — local files are operator-trusted and can be at any height) + if (requireCheckpoint && !Checkpoints::IsKnownCheckpoint(height, blockHash)) { fclose(file); strError = "Snapshot block " + blockHash.ToString() + " at height " + std::to_string(height) + " is not a known checkpoint"; diff --git a/src/utxosnapshot.h b/src/utxosnapshot.h index 5fb7a1b..81b8ef7 100644 --- a/src/utxosnapshot.h +++ b/src/utxosnapshot.h @@ -29,10 +29,14 @@ namespace UtxoSnapshot { // Load a UTXO snapshot from a file into a fresh LevelDB. // Writes block index entries, UTXOs, hashBestChain, and dbformat. // The LevelDB must NOT be open yet (call before LoadBlockIndex). + // `requireCheckpoint` enforces that the snapshot tip is a known + // checkpoint (for P2P-delivered snapshots). Local loads from a + // trusted operator pass false. // Returns true on success, sets strError on failure. bool LoadSnapshot(const std::filesystem::path& snapshotPath, const std::filesystem::path& dataDir, - std::string& strError); + std::string& strError, + bool requireCheckpoint); } // namespace UtxoSnapshot From d8af2aa17cb98dc3a3f2e33eea291fd271082e66 Mon Sep 17 00:00:00 2001 From: hermes Date: Thu, 18 Jun 2026 02:39:43 -0700 Subject: [PATCH 2/3] scripts: add sign-snapshot.sh for signed UTXO snapshot provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generates a UTXO snapshot via dumputxoset RPC, signs a provenance message (height, blockhash, snapshot sha256) with signmessage, and emits a signed manifest.json. Verification via ./sign-snapshot.sh verify or verifymessage RPC on any node. Pairs with the requireCheckpoint trust-gate patch — local snapshots no longer require a known checkpoint, so signing provenance is the way to establish authority for a snapshot. --- scripts/sign-snapshot.sh | 182 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100755 scripts/sign-snapshot.sh 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 From 677a8ea79a2f69da96faa6f4d87f39bf90d55377 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Fri, 19 Jun 2026 00:16:31 -0700 Subject: [PATCH 3/3] build: ignore build-*/ directories and build artifacts --- .gitignore | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 18cae18..576ad2a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,6 @@ blocks/ # IDE .vscode/ .idea/ -.claude/ *.swp *.swo *~ @@ -68,7 +67,6 @@ triangles.conf *.key *.cert *.gpg -*.o src/trianglesd src/obj/ build-bench/ @@ -78,3 +76,15 @@ build-latest/ build-rocks-probe/ build-rocksdb/ bench-results.csv + +# Local build dirs (krystie) +/build-*/ +/build/ +/bench-results.csv +/build-rocks-probe/ +/build-rocksdb/ +/build-cmake/ +/build-cmake-test/ +/build-latest/ +/build-bench/ +/.qmake.stash