Merge v5.9.17-local-snapshot-trust: signed UTXO snapshot infrastructure
Adds the foundation for the snapshot-based IBD: - sign-snapshot.sh: operator-side script to sign canonical snapshots - utxosnapshot gate requireCheckpoint on trust source - utxosnapshot build address index when loading (wallet balance support) - main build address index during FastImport - build: ignore build-*/ directories
This commit is contained in:
+12
-2
@@ -49,7 +49,6 @@ blocks/
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
.claude/
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
@@ -68,7 +67,6 @@ triangles.conf
|
|||||||
*.key
|
*.key
|
||||||
*.cert
|
*.cert
|
||||||
*.gpg
|
*.gpg
|
||||||
*.o
|
|
||||||
src/trianglesd
|
src/trianglesd
|
||||||
src/obj/
|
src/obj/
|
||||||
build-bench/
|
build-bench/
|
||||||
@@ -78,3 +76,15 @@ build-latest/
|
|||||||
build-rocks-probe/
|
build-rocks-probe/
|
||||||
build-rocksdb/
|
build-rocksdb/
|
||||||
bench-results.csv
|
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
|
||||||
|
|||||||
Executable
+182
@@ -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-<timestamp>.utx
|
||||||
|
# Output (in this dir):
|
||||||
|
# <snapshot-name> - the UTXO snapshot binary
|
||||||
|
# <snapshot-name>.sig - base64 signature
|
||||||
|
# <snapshot-name>.msg - signed message (human-readable provenance)
|
||||||
|
# <snapshot-name>.manifest.json - signed manifest (drop into bootstrap dir)
|
||||||
|
# <snapshot-name>.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 <manifest.json> <snapshot-file>
|
||||||
|
# OR via RPC:
|
||||||
|
# verifymessage <addr> <sig> <msg>
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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 <manifest.json> <snapshot-file>}"
|
||||||
|
SNAP="${3:?usage: $0 verify <manifest.json> <snapshot-file>}"
|
||||||
|
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"
|
||||||
+5
-2
@@ -792,8 +792,11 @@ bool DownloadUtxoSnapshot(const std::string& host,
|
|||||||
|
|
||||||
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
|
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
|
||||||
|
|
||||||
// Load the snapshot into a fresh active chain DB
|
// Load the snapshot into a fresh active chain DB. P2P-delivered
|
||||||
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) {
|
// 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);
|
fs::remove(tmpPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1025,8 +1025,13 @@ bool AppInit2()
|
|||||||
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
|
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
|
||||||
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
|
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;
|
std::string strError;
|
||||||
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) {
|
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) {
|
||||||
printf("UTXO snapshot loaded successfully.\n");
|
printf("UTXO snapshot loaded successfully.\n");
|
||||||
} else {
|
} else {
|
||||||
printf("UTXO snapshot load failed: %s\n", strError.c_str());
|
printf("UTXO snapshot load failed: %s\n", strError.c_str());
|
||||||
|
|||||||
+13
-3
@@ -214,11 +214,20 @@ static bool SnapAddressFromScript(const CScript& script, int& nType, uint160& ha
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// LoadSnapshot - load a UTXO snapshot into a fresh LevelDB
|
// 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,
|
bool LoadSnapshot(const fs::path& snapshotPath,
|
||||||
const fs::path& /*dataDir — unused; resolved per-backend via GetChainDataDir()*/,
|
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");
|
FILE* file = fopen(snapshotPath.string().c_str(), "rb");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -274,8 +283,9 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify snapshot block is a known checkpoint
|
// Verify snapshot block is a known checkpoint (only for P2P-delivered
|
||||||
if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) {
|
// snapshots — local files are operator-trusted and can be at any height)
|
||||||
|
if (requireCheckpoint && !Checkpoints::IsKnownCheckpoint(height, blockHash)) {
|
||||||
fclose(file);
|
fclose(file);
|
||||||
strError = "Snapshot block " + blockHash.ToString() + " at height "
|
strError = "Snapshot block " + blockHash.ToString() + " at height "
|
||||||
+ std::to_string(height) + " is not a known checkpoint";
|
+ std::to_string(height) + " is not a known checkpoint";
|
||||||
|
|||||||
+5
-1
@@ -29,10 +29,14 @@ namespace UtxoSnapshot {
|
|||||||
// Load a UTXO snapshot from a file into a fresh LevelDB.
|
// Load a UTXO snapshot from a file into a fresh LevelDB.
|
||||||
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
|
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
|
||||||
// The LevelDB must NOT be open yet (call before LoadBlockIndex).
|
// 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.
|
// Returns true on success, sets strError on failure.
|
||||||
bool LoadSnapshot(const std::filesystem::path& snapshotPath,
|
bool LoadSnapshot(const std::filesystem::path& snapshotPath,
|
||||||
const std::filesystem::path& dataDir,
|
const std::filesystem::path& dataDir,
|
||||||
std::string& strError);
|
std::string& strError,
|
||||||
|
bool requireCheckpoint);
|
||||||
|
|
||||||
} // namespace UtxoSnapshot
|
} // namespace UtxoSnapshot
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user