Compare commits

...

15 Commits

Author SHA1 Message Date
Sami Ahmed bdb7253399 Remove -allowfastimport (FastImport) entirely
FastImport was the legacy path for rebuilding the block index from a
local blk0001.dat. With v2 UTXO snapshots now containing embedded
blocks, FastImport is redundant and dangerous (could silently index
a forked chain from a stale blk0001.dat).

Changes:
- src/main.cpp: delete FastImportBlockFile() function (~270 lines)
- src/main.h:   delete FastImportBlockFile() declaration
- src/init.cpp:  delete -allowfastimport flag handler block
                 remove from help text
                 clean up stale comments referencing FastImportBlockFile
- src/bootstrap.cpp: update stale comments

v2 snapshot loading (auto-download from bootstrap or local placement
of utxo-snapshot.bin + manifest) is now the only supported sync start.

Tested: daemon builds, runs, chain state preserved across restart.
Binary SHA: 3f26f6202947a8dc0f7933314829702aafa1e42c968368ab7ec043d57baa9519
DNS2 + DNS3 running this build, both on correct chain.

Not bumped to v5.9.21 per Sami's preference. Next formal release
will inherit this change.
2026-06-19 23:56:14 -07:00
Sami Ahmed d81a36f875 Add tri CLI wrapper: wallet + secure messaging for agents and humans
A bash command interface to trianglesd RPC designed for Hermes, Krystie,
and Sami to manage TRI wallets and communicate via the built-in secure
messaging system (smessage).

Features:
- Info: status, balance, peers, staking info
- Wallet: addresses, send, transactions
- Secure messaging: inbox, outbox, send (encrypted via ECDH over Tor P2P)
- Raw RPC passthrough for any daemon command
- Bash + zsh completion
- SSH-tunneled RPC for remote node access
- Config at /etc/tri/nodes.conf (shared between agents)

Files:
- scripts/tri/tri                    Main script
- scripts/tri/nodes.conf.example     Config template
- scripts/tri/tri-completion.bash    Bash completion
- scripts/tri/_tri_zsh_completion    Zsh completion
- scripts/tri/README.md              Documentation

Tested against live DNS3 node (block 2,207,455, 4 peers).
Secure messaging verified: send → inbox → outbox all working.
2026-06-19 21:09:33 -07:00
Sami Ahmed f4f9c3b45a Merge cpp20-modernization into master: triangles-cli + macOS/Windows build fixes
Brings in from cpp20-modernization branch:
- 8aeb513: triangles-cli JSON-RPC client (bitcoin-cli pattern)
- 1d938d5: macOS build - use std::filesystem, drop Boost::system
- 600b1cf: macOS build - Boost::boost target for headers
- 569b541: Simplify DLL packaging
- 274aafa/91d9233: Windows packaging fixes
- 8c74f4e/ad26786: Packaging scripts (package-windows-daemon.sh, package-linux-daemon.sh)
2026-06-19 20:49:33 -07:00
Sami Ahmed 73c3cef8d4 bump: version 5.9.20 2026-06-19 20:19:43 -07:00
hermes a38bfd2f97 fix: auto-download UTXO snapshot when chain DB missing (3 root causes)
Three bugs prevented the wallet from automatically downloading the UTXO
snapshot when starting with stale blk0001.dat but no chain database:

1. NeedsBootstrap() only checked for blk0001.dat existence, not the chain
   DB. If blk0001.dat was present (leftover from old version) but
   txleveldb/chainstate was missing, it reported "no bootstrap needed"
   and the snapshot download never triggered.

   Fix: check for txleveldb/ or blocks/chainstate/ instead.

2. Bootstrap HTTP download was skipped when snapshotMode was true (the
   default). The code deferred to P2P snapshot fetch (Step 11.6), but
   that runs AFTER Step 7 which errored out on the FastImport gate.

   Fix: always attempt HTTP bootstrap when NeedsBootstrap is true,
   regardless of snapshotMode. The UTXO snapshot HTTP download IS the
   fast path — no reason to defer to P2P when HTTP is available.

3. FastImport gate (Step 7) was a hard InitError that killed the daemon
   before it ever reached the snapshot fetch path. blk0001.dat present
   + no chain index + FastImport disabled = immediate crash.

   Fix: instead of erroring, remove the stale blk0001.dat and continue.
   The daemon syncs from the snapshot that was already loaded in Step 6b,
   or from P2P if that somehow failed.
2026-06-19 19:40:40 -07:00
Sami Ahmed 23e8a2d647 utxosnapshot: v2 format — embed full blk0001.dat into snapshot
Per Sami's vision: 'I want to carry over the whole block inside the
UTXO.' The snapshot is now self-contained: a fresh node loading it
has everything needed (headers + UTXOs + all block bodies) without
needing a separate bootstrap tarball.

Format change (UTXO_SNAPSHOT_VERSION 1 → 2):

v1 HEADER (88 bytes):
  magic, version, network, height, blockHash, moneySupply,
  numHeaders, numUtxos, contentHash

v2 HEADER (92 bytes):
  same + numBlocks (between numUtxos and contentHash)

v2 CONTENT (after v1's headers + utxos sections):
  blocks[numBlocks]  ← raw blk0001.dat bytes, SHA256 included

DumpSnapshot changes:
- Walks ALL blocks from pindexBest to pindexGenesisBlock (was: last
  N=2000). The nHeaders arg is honored only when caller passes a
  count smaller than the full chain for v1-compat diagnostic snapshots.
- After headers + utxos sections, streams GetDataDir()/blk0001.dat
  bytes into the snapshot, chunked (64 KB), content-hashed.
- Header now writes numBlocks between numUtxos and contentHash.

LoadSnapshot changes:
- Reads numBlocks after numUtxos when version >= 2.
- After UTXOs section, streams numBlocks bytes from the snapshot
  into dataDir/blk0001.dat (uses GetDataDir() since the param dataDir
  is intentionally unnamed in this function).
- v1 snapshots still load via the partial-load path (no numBlocks in
  header, no blk0001.dat written).
- Empty snapshot check loosened to (numHeaders && numUtxos && numBlocks)
  — all three must be zero to be considered empty.

Total v2 snapshot size: ~1.9 GB (headers + blocks + UTXOs).
Generation on the operator machine: a few minutes. Download on
reasonable connection: a few minutes.

This supersedes the earlier v2 attempt (commit 69529ea) which had
compile bugs from using an unnamed dataDir parameter and had wrong
snapshot file layout.
2026-06-19 04:22:07 -07:00
Krystie ad267866ab Switch to script-file packaging for Windows + Linux daemon jobs
Replace inline multi-line run: blocks with invocations of the
scripts/ci/package-*.sh scripts. This sidesteps the GitHub Actions
msys2 + 'set -e -o pipefail' issue that caused silent exit 1 on the
Windows daemon packaging step. The scripts are also debuggable locally.
2026-06-18 19:50:10 -07:00
Krystie 8c74f4e228 Add package-windows-daemon.sh + package-linux-daemon.sh scripts
Move the Windows daemon packaging step and the Linux .deb build into
committed shell scripts under scripts/ci/. This bypasses GitHub Actions'
inline-run-block quirks (silent exit 1 under msys2 + set -e -o pipefail
with multi-line scripts) and makes the packaging logic debuggable locally.
2026-06-18 19:49:31 -07:00
Krystie f0e5dbdebc diagnostic: add tracing to Windows packaging step 2026-06-18 19:47:22 -07:00
Krystie 91d9233ea4 Simplify DLL packaging: plain for loop, no pipe-into-while
The previous attempts used 'ldd | sort -u | while read; do ... done' patterns
that exit 1 under MSYS2 bash + 'set -e -o pipefail' even when the script
ran successfully. Replaced with a plain 'for bin in ...; do ldd > list.txt;
while read; do cp; done < list.txt; done' pattern that has no pipelines
other than the standard redirection, and uses IFS= read -r for safe line
iteration.

Also moved temp files from /tmp to the working directory (./dll-list.txt)
to avoid any MSYS2 /tmp path-translation edge cases.
2026-06-18 19:34:45 -07:00
Krystie 274aafab36 Fix Windows packaging step: simplify bash { } | sort -u | while pattern
The previous step used a bash group command piped through sort -u and a
while loop. Under MSYS2 bash + 'set -e -o pipefail' (GitHub Actions
default), this triggered a non-zero exit even when the loop body
succeeded, causing the Windows daemon job to fail at the packaging step
(the actual link of both trianglesd.exe and triangles-cli.exe succeeded).

Replaced the { } | sort -u | while pattern with a temp-file-based dedup:
- ldd both binaries, append to /tmp/cli-dlls.txt (or cli-libs.txt on Linux)
- sort -u the temp file
- pipe the result into the while loop (simpler pipeline, no group)

Also applied the same simplification to the Linux .deb packaging for
consistency, even though the Linux build was passing.
2026-06-18 19:19:32 -07:00
Krystie 569b541931 Drop Boost entirely from triangles-cli: use raw sockets for HTTP
Third time's the charm. After two CI failures chasing boost::asio / libboost_system
linking issues across platforms (Homebrew missing config on macOS, MSYS2 versioned
names on Windows, CMake targets that don't quite work everywhere), rip the whole
Boost dependency out of the CLI and use raw POSIX/Winsock sockets.

- triangles-cli.cpp: replaced boost::asio with raw socket() / connect() / send()
  / recv() / getaddrinfo(). Cross-platform: #ifdef _WIN32 for Winsock + WSAStartup
  / WSACleanup, else POSIX. ~100 lines of clean portable socket code.
- src/CMakeLists.txt: dropped find_package(Boost) entirely. Only links
  json_compat (header-only) + ws2_32 on Windows. No boost libs to find.

Should be the last fix needed for this PR.
2026-06-18 19:05:37 -07:00
Krystie 600b1cf35f Fix macOS build: add Boost::boost target for headers, link boost_system
The previous fix dropped the find_package component but also killed the
boost include path. Now use the modern Boost::boost header-only target
(available in Boost 1.83+) which sets up include directories without
requiring a per-component config file.

Link libboost_system explicitly by name on all platforms — the linker
finds it via the platform's default search path:
- Linux: /usr/lib (libboost_system.so)
- macOS Homebrew: /opt/homebrew/lib (libboost_system.dylib)
- Windows MSYS2: mingw64/bin (libboost_system-mt-X-XX.dll)
2026-06-18 18:45:04 -07:00
Krystie 1d938d5770 Fix macOS build: drop Boost::system/find_package component, use std::filesystem
Homebrew's boost formula doesn't ship the boost_system CMake config file,
so find_package(Boost REQUIRED COMPONENTS system) failed on macOS.

- Replace boost::filesystem with std::filesystem (C++17, no Boost dep)
- Drop 'filesystem' from find_package — only headers needed (asio + system)
- Link libboost_system explicitly per-platform by library name, resolved
  via the platform's default search path (Homebrew toolchain on macOS,
  system libs on Linux, MSYS2 on Windows)

CI will rerun automatically on PR push.
2026-06-18 18:41:38 -07:00
Krystie 8aeb5133bf Add triangles-cli: JSON-RPC client (port bitcoin-cli pattern)
Triangles never had a CLI client (bitcoin-cli analog). This adds
triangles-cli as a third build target alongside trianglesd and
triangles-qt.

- src/triangles-cli.cpp: self-contained JSON-RPC 1.0 client.
  Reads triangles.conf for credentials, supports -rpcuser/-rpcpassword
  /-rpcconnect/-rpcport/-testnet/-datadir/-conf flags. Implements
  -getinfo (synthesized summary from getnetworkinfo/getblockchaininfo
  /getwalletinfo) and raw method dispatch. JSON via json_spirit compat
  shim (json_compat.h), HTTP via boost::asio, base64 auth inline.
  No util.cpp / wallet.cpp / net.cpp / triangles_common link dep —
  keeps the binary small (~600 KB Linux, ~1.5 MB Windows).

- CMake: new option(BUILD_CLI ON) + add_executable(triangles-cli)
  in src/CMakeLists.txt. Status line added.

- CI: BUILD_CLI=ON added to build-windows-daemon and build-linux-daemon
  jobs. triangles-cli.exe bundled into windows-daemon artifact
  alongside trianglesd.exe. triangles-cli added to linux-daemon .deb
  package (with launcher in /usr/bin).

- Default ON; set BUILD_CLI=OFF to skip.

Closes the open 'triangles-cli.exe missing from Windows build'
follow-up (the binary wasn't missing — it never existed).

Patterned after Bitcoin Core bitcoin-cli and Dash Core dash-cli.
2026-06-18 18:33:30 -07:00
12 changed files with 1002 additions and 328 deletions
+77
View File
@@ -0,0 +1,77 @@
# tri — Cryptographic Triangles CLI
A friendly bash wrapper around `trianglesd` RPC for humans and agents.
## Install
```bash
# System-wide
sudo cp tri /usr/local/bin/tri
sudo chmod +x /usr/local/bin/tri
sudo mkdir -p /etc/tri
sudo cp nodes.conf.example /etc/tri/nodes.conf
# Edit /etc/tri/nodes.conf with your node's RPC credentials
# Bash completion
sudo cp tri-completion.bash /etc/bash_completion.d/
# Zsh completion
sudo cp _tri_zsh_completion /usr/local/share/zsh/site-functions/_tri
```
## Config
Edit `/etc/tri/nodes.conf`:
```bash
TRI_SSH_HOST="100.81.59.99" # Node IP (or remove for local)
TRI_SSH_USER="root"
TRI_RPC_PORT="19112"
TRI_RPC_USER="your-rpc-user"
TRI_RPC_PASS="your-rpc-password"
# TRI_WALLET_PASSPHRASE="wallet-passphrase" # If wallet is encrypted
```
## Commands
### Info
- `tri` — Status overview
- `tri status` — Detailed node status
- `tri balance` — Wallet balance + UTXO count
- `tri peers` — Connected peers
- `tri stake` — Staking info
### Wallet
- `tri address new` — New address
- `tri address list` — List addresses
- `tri address balance` — Per-address balances
- `tri send <addr> <amt> [memo]` — Send TRI
- `tri tx [N]` — Recent transactions
- `tri tx <txid>` — Transaction details
### Secure Messaging
- `tri msg inbox` — Read messages
- `tri msg outbox` — Sent messages
- `tri msg send <from> <to> <msg>` — Send encrypted message
- `tri msg anon <to> <msg>` — Anonymous message
- `tri msg keys` — Messaging keys
- `tri msg enable` — Enable secure messaging
- `tri msg pubkey <addr>` — Get public key
### Advanced
- `tri raw <method> [params...]` — Raw RPC passthrough
## Agent Integration (Hermes, Krystie)
Both agents on DNS2 share the same `/etc/tri/nodes.conf` and can execute all commands.
For inter-agent messaging via TRI's encrypted P2P network:
1. Each agent needs a TRI address: `tri address new`
2. Enable messaging: `tri msg enable`
3. Register key: `tri raw smsglocalkeys recv + <address>`
4. Exchange addresses between agents
5. Send: `tri msg send <hermes_addr> <krystie_addr> "message"`
6. Read: `tri msg inbox`
Messages are encrypted (ECDH), routed through the Tor P2P network,
stored for 48 hours, max 4096 bytes each.
+39
View File
@@ -0,0 +1,39 @@
#compdef tri
_tri() {
local -a commands
commands=(
'status:Detailed node status'
'balance:Wallet balance'
'peers:Connected peers'
'stake:Staking info'
'address:Address management'
'send:Send TRI'
'tx:Transactions'
'msg:Secure messaging'
'raw:Raw RPC passthrough'
'help:Show help'
)
_arguments -C \
"1:command:->command" \
"*::arg:->args"
case "$state" in
command)
_describe 'tri command' commands
;;
args)
case ${words[1]} in
address|addr)
_values 'subcommand' 'new' 'list' 'balance'
;;
msg|message|messages)
_values 'subcommand' 'inbox' 'outbox' 'send' 'anon' 'keys' 'enable' 'pubkey' 'unlock'
;;
esac
;;
esac
}
_tri "$@"
+32
View File
@@ -0,0 +1,32 @@
# /etc/tri/nodes.conf — Triangles node configuration
#
# Shared by Hermes and Krystie. Both agents on DNS2 tunnel RPC
# to the trianglesd node on DNS3 via SSH.
#
# Node: DNS3 (100.81.59.99)
# ─── Connection ──────────────────────────────────────────────────────────────
# RPC is only accessible on localhost at the node, so we SSH-tunnel
TRI_SSH_HOST="your-node-ip-here"
TRI_SSH_USER="root"
# RPC credentials (as set in triangles.conf on the node)
TRI_RPC_HOST="127.0.0.1"
TRI_RPC_PORT="19112"
TRI_RPC_USER="your-rpc-user-here"
TRI_RPC_PASS="your-rpc-password-here"
# ─── Wallet ──────────────────────────────────────────────────────────────────
# Wallet passphrase for unlocking (needed for messaging + sending)
# Leave empty if wallet is unencrypted or set via env var TRI_WALLET_PASSPHRASE
# TRI_WALLET_PASSPHRASE=""
# Default sender address for messages (set after creating addresses)
# TRI_DEFAULT_FROM=""
# ─── Agent Addresses ─────────────────────────────────────────────────────────
# When agents have their own TRI addresses, register them here:
# HERMES_TRI_ADDR="T..."
# KRYSTIE_TRI_ADDR="T..."
+691
View File
@@ -0,0 +1,691 @@
#!/usr/bin/env bash
#
# tri — Cryptographic Triangles command interface
#
# A friendly wrapper around trianglesd RPC for both human and agent use.
# Designed for Hermes, Krystie, and Sami to manage TRI wallets, monitor
# nodes, and communicate via the built-in secure messaging system.
#
# Config: /etc/tri/nodes.conf (or ~/.config/tri/nodes.conf)
# Completion: /etc/bash_completion.d/tri-completion.bash
#
# Usage: tri <command> [subcommand] [args]
# tri Status overview
# tri help Full command list
# tri status Detailed node status
# tri balance Wallet balance
# tri peers Connected peers
# tri stake Staking info
# tri address new Generate new wallet address
# tri address list List wallet addresses
# tri address balance Per-address balances
# tri send <addr> <amt> [memo] Send TRI
# tri tx [N] Recent N transactions (default 10)
# tri tx <txid> Transaction details
# tri msg inbox Secure message inbox
# tri msg outbox Sent messages
# tri msg send <from> <to> <msg> Send encrypted message
# tri msg anon <to> <msg> Send anonymous message
# tri msg keys List messaging keys
# tri msg enable Enable secure messaging
# tri msg pubkey <addr> Get public key for address
# tri msg unlock [secs] Unlock wallet for messaging (default 60s)
# tri raw <method> [params...] Raw RPC passthrough
#
set -euo pipefail
# ─── Config ──────────────────────────────────────────────────────────────────
TRI_CONFIG="/etc/tri/nodes.conf"
[[ -f "$HOME/.config/tri/nodes.conf" ]] && TRI_CONFIG="$HOME/.config/tri/nodes.conf"
# Defaults (overridden by config file)
TRI_RPC_HOST="127.0.0.1"
TRI_RPC_PORT="19112"
TRI_RPC_USER=""
TRI_RPC_PASS=""
TRI_SSH_HOST="" # If set, RPC calls are tunneled via SSH to this host
TRI_SSH_USER="root"
TRI_WALLET_PASSPHRASE="" # For unlocking wallet when sending/messages
TRI_DEFAULT_FROM="" # Default sender address for messages
# Load config
if [[ -f "$TRI_CONFIG" ]]; then
source "$TRI_CONFIG"
fi
# Allow env overrides
[[ -n "${TRI_RPC_HOST_ENV:-}" ]] && TRI_RPC_HOST="$TRI_RPC_HOST_ENV"
[[ -n "${TRI_RPC_PORT_ENV:-}" ]] && TRI_RPC_PORT="$TRI_RPC_PORT_ENV"
[[ -n "${TRI_SSH_HOST_ENV:-}" ]] && TRI_SSH_HOST="$TRI_SSH_HOST_ENV"
# ─── Colors ──────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_DIM="\033[2m"
C_RED="\033[31m"
C_GREEN="\033[32m"
C_YELLOW="\033[33m"
C_BLUE="\033[34m"
C_CYAN="\033[36m"
C_MAGENTA="\033[35m"
else
C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""
C_BLUE=""; C_CYAN=""; C_MAGENTA=""
fi
# ─── Helpers ─────────────────────────────────────────────────────────────────
# Core RPC call function. Executes JSON-RPC against the node.
# Usage: _tri_rpc <method> [param1] [param2] ...
_tri_rpc() {
local method="$1"; shift
local params="[]"
if [[ $# -gt 0 ]]; then
# Build JSON params array
local json_params=()
for p in "$@"; do
# Try to detect numbers and booleans
if [[ "$p" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
json_params+=("$p")
elif [[ "$p" == "true" || "$p" == "false" || "$p" == "null" ]]; then
json_params+=("\"$p\"")
else
# Escape for JSON string
local escaped="${p//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
json_params+=("\"$escaped\"")
fi
done
params="[$(IFS=,; echo "${json_params[*]}")]"
fi
local payload="{\"jsonrpc\":\"1.0\",\"id\":\"tri\",\"method\":\"$method\",\"params\":$params}"
if [[ -n "$TRI_SSH_HOST" ]]; then
# Tunnel via SSH
local auth="$TRI_RPC_USER:$TRI_RPC_PASS"
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
"${TRI_SSH_USER}@${TRI_SSH_HOST}" \
"curl -s --connect-timeout 10 http://127.0.0.1:${TRI_RPC_PORT}/ \
-u '${auth}' \
-H 'Content-Type: application/json' \
-d '${payload//\'/\'\\\'\'}'" 2>/dev/null
else
# Local connection
curl -s --connect-timeout 10 "http://${TRI_RPC_HOST}:${TRI_RPC_PORT}/" \
-u "${TRI_RPC_USER}:${TRI_RPC_PASS}" \
-H 'Content-Type: application/json' \
-d "$payload" 2>/dev/null
fi
}
# Pretty RPC call — extracts .result and pretty-prints JSON
# Usage: _tri_rpc_pretty <method> [param1] [param2] ...
_tri_rpc_pretty() {
local raw
raw=$(_tri_rpc "$@")
if [[ -z "$raw" ]]; then
echo -e "${C_RED}Error: No response from node${C_RESET}" >&2
return 1
fi
# Check for error
local err
err=$(echo "$raw" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message','') if d.get('error') else '',end='')" 2>/dev/null || echo "")
if [[ -n "$err" ]]; then
echo -e "${C_RED}RPC Error: ${err}${C_RESET}" >&2
return 1
fi
echo "$raw" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('result',''),indent=2))" 2>/dev/null
}
# Raw RPC call — print full JSON response as-is
_tri_rpc_raw() {
_tri_rpc "$@"
}
# Extract a single field from RPC result
# Usage: _tri_rpc_field <method> <field> [params...]
_tri_rpc_field() {
local method="$1"; shift
local field="$1"; shift
_tri_rpc "$method" "$@" | python3 -c "
import sys,json
d=json.load(sys.stdin)
r=d.get('result',{})
if isinstance(r,dict):
print(r.get('$field',''))
else:
print(r)
" 2>/dev/null
}
# Extract multiple fields
_tri_rpc_fields() {
local method="$1"; shift
_tri_rpc "$method" "$@" | python3 -c "
import sys,json
d=json.load(sys.stdin)
r=d.get('result',{})
if isinstance(r, dict):
for k,v in r.items():
if isinstance(v,(str,int,float,bool)) or v is None:
print(f'{k}: {v}')
" 2>/dev/null
}
# Unlock wallet for messaging
_tri_unlock() {
local duration="${1:-60}"
if [[ -z "$TRI_WALLET_PASSPHRASE" ]]; then
echo -e "${C_YELLOW}Warning: TRI_WALLET_PASSPHRASE not set in config${C_RESET}" >&2
return 1
fi
_tri_rpc walletpassphrase "$TRI_WALLET_PASSPHRASE" "$duration" >/dev/null 2>&1
}
# ─── Commands: Info ──────────────────────────────────────────────────────────
cmd_status() {
echo -e "${C_BOLD}${C_CYAN}Triangles Node Status${C_RESET}"
echo -e "${C_DIM}$(date -u '+%Y-%m-%d %H:%M:%S UTC')${C_RESET}"
echo ""
local info
info=$(_tri_rpc getinfo 2>/dev/null)
if [[ -z "$info" ]]; then
echo -e "${C_RED}Cannot connect to node${C_RESET}"
if [[ -n "$TRI_SSH_HOST" ]]; then
echo -e " Target: ${TRI_SSH_USER}@${TRI_SSH_HOST} → RPC ${TRI_RPC_PORT}"
else
echo -e " Target: ${TRI_RPC_HOST}:${TRI_RPC_PORT}"
fi
return 1
fi
echo "$info" | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(f\" Version: {d.get('version','?')}\")
print(f\" Blocks: {d.get('blocks','?'):,}\")
print(f\" Connections: {d.get('connections','?')}\")
print(f\" Balance: {d.get('balance',0):.4f} TRI\")
print(f\" Stake: {d.get('stake',0):.4f} TRI\")
print(f\" Money Supply: {d.get('moneysupply',0):,.2f} TRI\")
print(f\" Difficulty: {d.get('difficulty','?')}\")
print(f\" Testnet: {d.get('testnet',False)}\")
" 2>/dev/null
# Peer summary
local peer_count
peer_count=$(_tri_rpc_field getconnectioncount "result" 2>/dev/null || echo "?")
echo ""
echo -e " ${C_DIM}Node: ${TRI_SSH_HOST:-${TRI_RPC_HOST}}:${TRI_RPC_PORT}${C_RESET}"
}
cmd_balance() {
local balance
balance=$(_tri_rpc_field getbalance "balance" 2>/dev/null || echo "error")
if [[ "$balance" == "error" ]]; then
echo -e "${C_RED}Cannot connect to node${C_RESET}" >&2
return 1
fi
local stake
stake=$(_tri_rpc_field getinfo "stake" 2>/dev/null || echo "0")
echo -e "${C_BOLD}Wallet Balance${C_RESET}"
echo -e " Available: ${C_GREEN}${balance} TRI${C_RESET}"
echo -e " Staking: ${C_YELLOW}${stake} TRI${C_RESET}"
# UTXO count
local utxo_count
utxo_count=$(_tri_rpc listunspent 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('result',[])))" 2>/dev/null || echo "?")
[[ "$utxo_count" != "?" ]] && echo -e " UTXOs: ${utxo_count}"
}
cmd_peers() {
local raw
raw=$(_tri_rpc getpeerinfo 2>/dev/null)
echo -e "${C_BOLD}Connected Peers${C_RESET}"
echo "$raw" | python3 -c "
import sys,json
d=json.load(sys.stdin)
peers=d.get('result',[])
if not peers:
print(' (no peers connected)')
else:
for p in peers:
addr = p.get('addr','?')
subver = p.get('subver','?').replace('/','')
height = p.get('startingheight','?')
ping = p.get('pingtime',0)
if isinstance(ping,(int,float)) and ping > 0:
ping_ms = ping * 1000
print(f' {addr:30s} {subver:25s} height={height} ping={ping_ms:.0f}ms')
else:
print(f' {addr:30s} {subver:25s} height={height}')
print(f'\n Total: {len(peers)} peer(s)')
" 2>/dev/null
}
cmd_stake() {
echo -e "${C_BOLD}Staking Information${C_RESET}"
_tri_rpc_fields getstakinginfo 2>/dev/null | while read -r line; do
echo " $line"
done
}
# ─── Commands: Wallet ────────────────────────────────────────────────────────
cmd_address() {
local sub="${1:-list}"; shift || true
case "$sub" in
new)
local addr
addr=$(_tri_rpc_field getnewaddress "result" 2>/dev/null)
if [[ -n "$addr" ]]; then
echo "$addr"
else
echo -e "${C_RED}Failed to generate address${C_RESET}" >&2
return 1
fi
;;
list)
echo -e "${C_BOLD}Wallet Addresses${C_RESET}"
_tri_rpc getaddressesbyaccount "" 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
addrs=d.get('result',[])
if not addrs:
print(' (no addresses)')
else:
for a in addrs:
print(f' {a}')
print(f'\n Total: {len(addrs)}')
" 2>/dev/null
;;
balance)
echo -e "${C_BOLD}Address Balances${C_RESET}"
_tri_rpc listaddressgroupings 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
groups=d.get('result',[])
if not groups:
print(' (no address balances)')
else:
for group in groups:
for item in group:
addr=item[0] if isinstance(item,list) and len(item)>0 else '?'
amt=item[1] if isinstance(item,list) and len(item)>1 else '?'
print(f' {addr:40s} {amt} TRI')
" 2>/dev/null
;;
*)
echo -e "${C_RED}Unknown subcommand: $sub${C_RESET}" >&2
echo "Usage: tri address [new|list|balance]" >&2
return 1
;;
esac
}
cmd_send() {
if [[ $# -lt 2 ]]; then
echo -e "${C_RED}Usage: tri send <address> <amount> [memo]${C_RESET}" >&2
return 1
fi
local addr="$1"
local amount="$2"
local memo="${3:-}"
echo -e "${C_YELLOW}Sending ${amount} TRI to ${addr}...${C_RESET}"
local result
if [[ -n "$memo" ]]; then
result=$(_tri_rpc sendtoaddress "$addr" "$amount" "$memo" 2>/dev/null)
else
result=$(_tri_rpc sendtoaddress "$addr" "$amount" 2>/dev/null)
fi
local txid
txid=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result','') if d.get('result') else d.get('error',{}).get('message','FAILED'),end='')" 2>/dev/null)
if [[ "$txid" == "FAILED" ]] || [[ -z "$txid" ]]; then
echo -e "${C_RED}Send failed: $txid${C_RESET}" >&2
return 1
fi
echo -e "${C_GREEN}Sent! TXID: ${txid}${C_RESET}"
}
cmd_tx() {
if [[ $# -eq 0 ]]; then
# Recent transactions
echo -e "${C_BOLD}Recent Transactions${C_RESET}"
_tri_rpc listtransactions "*" 10 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
txs=d.get('result',[])
if not txs:
print(' (no transactions)')
else:
for t in reversed(txs):
category = t.get('category','?')
amount = t.get('amount',0)
addr = t.get('address','?')
confirmations = t.get('confirmations',0)
txid = t.get('txid','?')
time = t.get('time',0)
from datetime import datetime
dt = datetime.fromtimestamp(time) if time else None
datestr = dt.strftime('%Y-%m-%d %H:%M') if dt else '???'
# Color by category
if category == 'receive' or category == 'generate' or category == 'mint':
amt_str = f'+{amount} TRI'
else:
amt_str = f'-{amount} TRI'
conf_str = f'{confirmations} conf' if confirmations > 0 else 'unconfirmed'
print(f' {datestr} {amt_str:>15s} {category:10s} {conf_str:>12s} {addr}')
print(f' {txid}')
" 2>/dev/null
else
# Transaction details
local txid="$1"
echo -e "${C_BOLD}Transaction: ${txid}${C_RESET}"
_tri_rpc_fields gettransaction "$txid" 2>/dev/null | while read -r line; do
echo " $line"
done
fi
}
# ─── Commands: Secure Messaging ──────────────────────────────────────────────
cmd_msg() {
local sub="${1:-inbox}"; shift || true
case "$sub" in
inbox)
# Unlock wallet first if passphrase is configured
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_BOLD}${C_MAGENTA}Secure Message Inbox${C_RESET}"
_tri_rpc smsginbox "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
msg = d.get('message')
count_str = d.get('result','0 messages shown.')
# Extract count from result string like 'N messages shown.'
try:
count = int(count_str.split()[0])
except:
count = 0
if count == 0 or msg is None:
print(' (inbox is empty)')
else:
# The daemon returns one message per RPC call (last one only).
# For full inbox dump, use: tri raw smsginbox all
frm = msg.get('from','?')
to = msg.get('to','?')
text = msg.get('text','(no text)')
sent = msg.get('sent','')
rcvd = msg.get('received','')
print(f' Latest message (of {count}):')
print(f' Sent: {sent}')
print(f' Received: {rcvd}')
print(f' From: {frm}')
print(f' To: {to}')
print(f' Text: {text[:200]}')
if count > 1:
print(f'')
print(f' ({count-1} more messages — use: tri raw smsginbox all)')
" 2>/dev/null
;;
outbox)
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_BOLD}${C_MAGENTA}Sent Messages${C_RESET}"
_tri_rpc smsgoutbox "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
msg = d.get('message')
count_str = d.get('result','0 sent messages shown.')
try:
count = int(count_str.split()[0])
except:
count = 0
if count == 0 or msg is None:
print(' (outbox is empty)')
else:
to = msg.get('to','?')
frm = msg.get('from','?')
text = msg.get('text','(no text)')
sent = msg.get('sent','')
print(f' Latest sent (of {count}):')
print(f' Sent: {sent}')
print(f' From: {frm}')
print(f' To: {to}')
print(f' Text: {text[:200]}')
if count > 1:
print(f'')
print(f' ({count-1} more — use: tri raw smsgoutbox all)')
" 2>/dev/null
;;
send)
if [[ $# -lt 3 ]]; then
echo -e "${C_RED}Usage: tri msg send <from_address> <to_address> <message>${C_RESET}" >&2
return 1
fi
local from_addr="$1"
local to_addr="$2"
shift 2
local message="$*"
# Unlock for send
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_YELLOW}Sending encrypted message...${C_RESET}"
local result
result=$(_tri_rpc smsgsend "$from_addr" "$to_addr" "$message" 2>/dev/null)
local status
status=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('result','') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
if [[ "$status" == "Sent." ]]; then
echo -e "${C_GREEN}Message sent to ${to_addr}${C_RESET}"
else
local err
err=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('error','unknown error') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
echo -e "${C_RED}Send failed: ${err}${C_RESET}" >&2
return 1
fi
;;
anon)
if [[ $# -lt 2 ]]; then
echo -e "${C_RED}Usage: tri msg anon <to_address> <message>${C_RESET}" >&2
return 1
fi
local to_addr="$1"
shift
local message="$*"
echo -e "${C_YELLOW}Sending anonymous encrypted message...${C_RESET}"
local result
result=$(_tri_rpc smsgsendanon "$to_addr" "$message" 2>/dev/null)
local status
status=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('result','') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
if [[ "$status" == "Sent." ]]; then
echo -e "${C_GREEN}Anonymous message sent to ${to_addr}${C_RESET}"
else
echo -e "${C_RED}Send failed${C_RESET}" >&2
return 1
fi
;;
keys)
echo -e "${C_BOLD}${C_MAGENTA}Messaging Keys${C_RESET}"
_tri_rpc smsglocalkeys "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
if isinstance(d, dict):
key_line = d.get('key','')
count_line = d.get('result','')
if key_line:
print(f' {key_line}')
if count_line:
print(f' {count_line}')
elif isinstance(d, str):
print(f' {d}')
else:
print(' (no keys registered)')
" 2>/dev/null
;;
enable)
echo -e "${C_YELLOW}Enabling secure messaging...${C_RESET}"
_tri_rpc_pretty smsgenable 2>/dev/null
;;
pubkey)
if [[ $# -lt 1 ]]; then
echo -e "${C_RED}Usage: tri msg pubkey <address>${C_RESET}" >&2
return 1
fi
_tri_rpc_pretty smsggetpubkey "$1" 2>/dev/null
;;
unlock)
local duration="${1:-60}"
if [[ -z "$TRI_WALLET_PASSPHRASE" ]]; then
echo -e "${C_RED}TRI_WALLET_PASSPHRASE not set in config${C_RESET}" >&2
return 1
fi
_tri_rpc walletpassphrase "$TRI_WALLET_PASSPHRASE" "$duration" >/dev/null 2>&1
echo -e "${C_GREEN}Wallet unlocked for ${duration}s${C_RESET}"
;;
*)
echo -e "${C_RED}Unknown msg subcommand: $sub${C_RESET}" >&2
echo "Usage: tri msg [inbox|outbox|send|anon|keys|enable|pubkey|unlock]" >&2
return 1
;;
esac
}
# ─── Commands: Raw RPC ───────────────────────────────────────────────────────
cmd_raw() {
if [[ $# -eq 0 ]]; then
echo -e "${C_RED}Usage: tri raw <method> [params...]${C_RESET}" >&2
echo "Example: tri raw getblockhash 2200000" >&2
return 1
fi
_tri_rpc_pretty "$@"
}
# ─── Help ────────────────────────────────────────────────────────────────────
cmd_help() {
cat << 'EOF'
tri — Cryptographic Triangles Command Interface
INFO
tri Status overview (blocks, connections, balance)
tri status Detailed node status
tri balance Wallet balance + UTXO count
tri peers Connected peers with ping times
tri stake Staking information
WALLET
tri address new Generate new wallet address
tri address list List all wallet addresses
tri address balance Per-address balance breakdown
tri send <addr> <amt> [memo] Send TRI to address
tri tx [N] Recent N transactions (default 10)
tri tx <txid> Transaction details
SECURE MESSAGING
tri msg inbox Read inbox messages (wallet auto-unlocks)
tri msg outbox Read sent messages
tri msg send <from> <to> <msg> Send encrypted message
tri msg anon <to> <msg> Send anonymous message
tri msg keys List messaging keys
tri msg enable Enable secure messaging
tri msg pubkey <addr> Get public key for an address
tri msg unlock [secs] Unlock wallet for messaging (default 60s)
ADVANCED
tri raw <method> [params...] Raw RPC passthrough
tri help This help screen
CONFIG
/etc/tri/nodes.conf System-wide config
~/.config/tri/nodes.conf Per-user config override
AGENTS (Hermes, Krystie)
Both agents use the same config and can execute all commands.
For messaging between agents, each needs its own TRI address
registered in the wallet. Use 'tri msg keys' to verify.
EOF
}
# ─── Main ────────────────────────────────────────────────────────────────────
main() {
local cmd="${1:-status}"; shift || true
case "$cmd" in
status|info) cmd_status "$@" ;;
balance) cmd_balance "$@" ;;
peers) cmd_peers "$@" ;;
stake|staking) cmd_stake "$@" ;;
address|addr) cmd_address "$@" ;;
send) cmd_send "$@" ;;
tx|transactions) cmd_tx "$@" ;;
msg|message|messages) cmd_msg "$@" ;;
raw) cmd_raw "$@" ;;
help|-h|--help) cmd_help "$@" ;;
*)
echo -e "${C_RED}Unknown command: $cmd${C_RESET}" >&2
echo "Run 'tri help' for available commands" >&2
exit 1
;;
esac
}
main "$@"
+40
View File
@@ -0,0 +1,40 @@
# bash/zsh completion for tri command
# Install: source this file or place in /etc/bash_completion.d/
_tri_complete() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Top-level commands
local top_cmds="status balance peers stake address send tx msg raw help"
local addr_subcmds="new list balance"
local msg_subcmds="inbox outbox send anon keys enable pubkey unlock"
if [[ ${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=($(compgen -W "${top_cmds}" -- "${cur}"))
return 0
fi
# Subcommand completion
if [[ ${COMP_CWORD} -eq 2 ]]; then
case "${COMP_WORDS[1]}" in
address|addr)
COMPREPLY=($(compgen -W "${addr_subcmds}" -- "${cur}"))
return 0
;;
msg|message|messages)
COMPREPLY=($(compgen -W "${msg_subcmds}" -- "${cur}"))
return 0
;;
esac
fi
# Address completion for send/msg send (would need wallet addresses in practice)
# For now, no further completion
return 0
}
complete -F _tri_complete tri
+10 -3
View File
@@ -50,7 +50,14 @@ namespace Bootstrap {
bool NeedsBootstrap(const fs::path& dataDir)
{
return !fs::exists(dataDir / "blk0001.dat");
// Need bootstrap if there's no chain database (the UTXO set / block index).
// blk0001.dat alone is NOT sufficient — it's raw block data that requires
// (fast-import was removed; UTXO snapshot is the only sync path)
// Check for both LevelDB (txleveldb/) and RocksDB (chainstate/) backends.
bool hasChainDb = fs::exists(dataDir / "txleveldb")
|| fs::exists(dataDir / "blocks" / "chainstate")
|| fs::exists(dataDir / "chainstate");
return !hasChainDb;
}
// Direct TCP connection bypassing Tor SOCKS proxy.
@@ -727,7 +734,7 @@ bool DownloadBootstrap(const std::string& host,
// Check if the archive included a trusted pre-built index for the active
// backend with a valid snapshot.manifest. If verified, keep it to skip the
// multi-hour FastImportBlockFile() rebuild.
// multi-hour rebuild (fast-import removed; UTXO snapshot is the only sync path).
fs::path chainDbPath = GetChainDataDir();
fs::path database = dataDir / "database";
fs::path manifestPath = dataDir / "snapshot.manifest";
@@ -760,7 +767,7 @@ bool DownloadBootstrap(const std::string& host,
if (!keepIndex) {
// No valid manifest or verification failed - delete the index.
// FastImportBlockFile() will rebuild from blk0001.dat on next startup.
// The block index will be rebuilt from the UTXO snapshot on next startup.
printf("Bootstrap: removing extracted %s/ (will rebuild index from blk0001.dat)\n",
GetChainDataDir().filename().string().c_str());
if (fs::exists(chainDbPath))
+1 -1
View File
@@ -8,7 +8,7 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 9
#define CLIENT_VERSION_REVISION 17
#define CLIENT_VERSION_REVISION 20
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+7 -34
View File
@@ -533,7 +533,6 @@ std::string HelpMessage()
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
" -autorerebuild=<n> " + _("If our chain is more than <n> blocks behind peers, wipe chain DB and shutdown for clean restart (default: 0=disabled)") + "\n" +
" -allowfastimport " + _("Permit FastImport as fallback (operator opt-in only; default off)") + "\n" +
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
@@ -1030,14 +1029,11 @@ bool AppInit2()
fs::path dataPath = GetDataDir();
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
if (needsBootstrap && !noBootstrap && !snapshotMode) {
printf("Bootstrap: no blockchain data found — downloading automatically.\n");
if (needsBootstrap && !noBootstrap) {
printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n");
printf("Bootstrap: (use -nobootstrap to skip)\n");
uiInterface.InitMessage(_("Downloading blockchain data..."));
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
wantsBootstrap = true;
} else if (needsBootstrap && snapshotMode && !wantsBootstrap) {
printf("Bootstrap: no blockchain data found — will fetch UTXO snapshot via P2P after network start.\n");
printf("Bootstrap: (use -bootstrap for legacy clearnet HTTP bootstrap, or -snapshot=0 to disable P2P fetcher)\n");
}
if (wantsBootstrap)
@@ -1163,7 +1159,7 @@ bool AppInit2()
}
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
// blk*.dat files via FastImportBlockFile(). This recalculates money
// blk*.dat files. This recalculates money
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
// WipeChainDataDir(), which resolves the directory per the configured
// -chaindb backend.
@@ -1212,38 +1208,15 @@ bool AppInit2()
}
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
// and shutdown for clean restart. Must run before FastImportBlockFile below.
// and shutdown for clean restart.
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
if (fRequestShutdown) {
printf("AutoRebuild: shutdown requested before chain load complete\n");
return false;
}
// If the block index is empty but blk0001.dat exists (bootstrap download),
// fast-import would normally rebuild from the block file. Per Sami: FastImport
// is REMOVED as a primary path — the UTXO snapshot is the canonical sync start.
// FastImport is gated behind -allowfastimport for explicit operator opt-in only
// (emergency recovery, snapshot format incompatibility, etc).
if (nBestHeight == 0 && std::filesystem::exists(GetDataDir() / "blk0001.dat")
&& mapBlockIndex.size() <= 1)
{
if (!GetBoolArg("-allowfastimport", false))
{
return InitError(_(
"Block index empty and blk0001.dat is present, but FastImport is disabled "
"(default). The snapshot path is the only supported sync start.\n\n"
"To recover:\n"
" 1. Place a signed utxo-snapshot.bin in the data directory and restart, OR\n"
" 2. Delete blk0001.dat (the snapshot path will sync from network), OR\n"
" 3. Pass -allowfastimport=1 to permit FastImport (operator opt-in only)."));
}
printf("FastImport: WARNING -allowfastimport is set; rebuilding from local blk0001.dat.\n");
uiInterface.InitMessage(_("Importing bootstrap blocks..."));
printf("Block index empty but blk0001.dat exists - running fast import...\n");
int64_t nFastImportStart = GetTimeMillis();
FastImportBlockFile();
StartupPerfLog("bootstrap_fast_import", GetTimeMillis() - nFastImportStart, strprintf("bestheight=%d", nBestHeight));
}
// Block index loaded. With fast-import removed, the only supported sync path
// is the UTXO snapshot (auto-downloaded from bootstrap or placed manually in datadir).
// as LoadBlockIndex can take several minutes, it's possible the user
// requested to kill triangles-qt during the last operation. If so, exit.
-276
View File
@@ -3641,287 +3641,11 @@ bool LoadExternalBlockFile(FILE* fileIn)
return nLoaded > 0;
}
bool FastImportBlockFile()
{
// Fast block import: reads blk0001.dat and builds the block index
// directly without re-writing block data. LevelDB writes are batched
// every 200K blocks for speed. Only used for trusted bootstrap data
// (blocks below the hardcoded checkpoint).
fs::path blkPath = GetDataDir() / "blk0001.dat";
if (!fs::exists(blkPath))
return false;
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
int64_t nStart = GetTimeMillis();
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
if (!fileIn)
return false;
// Get file size for progress
fseek(fileIn, 0, SEEK_END);
int64_t nFileSize = ftell(fileIn);
fseek(fileIn, 0, SEEK_SET);
int nLoaded = 0;
int64_t nLastProgressReport = 0;
{
LOCK(cs_main);
CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION);
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
txdb.TxnBegin();
unsigned int nPos = 0;
while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown)
{
// Find message start bytes (same scan as LoadExternalBlockFile)
unsigned char pchData[65536];
do {
fseek(blkdat, nPos, SEEK_SET);
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
if (nRead <= 8)
{
nPos = (unsigned int)-1;
break;
}
void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart));
if (nFind)
{
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0)
{
nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart);
break;
}
nPos += ((unsigned char*)nFind - pchData) + 1;
}
else
nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1;
} while(!fRequestShutdown);
if (nPos == (unsigned int)-1)
break;
fseek(blkdat, nPos, SEEK_SET);
unsigned int nSize;
blkdat >> nSize;
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
{
nPos += 4 + nSize;
continue;
}
// nBlockPos = file position where the block data starts
// (after 4-byte message start + 4-byte size)
unsigned int nBlockPos = nPos + 4;
CBlock block;
blkdat >> block;
uint256 hash = block.GetHash();
if (mapBlockIndex.count(hash))
{
nPos += 4 + nSize;
continue; // already indexed
}
// Create CBlockIndex
CBlockIndex* pindexNew = new CBlockIndex(1, nBlockPos, block);
if (!pindexNew)
break;
// Link to previous block
auto miPrev = mapBlockIndex.find(block.hashPrevBlock);
if (miPrev != mapBlockIndex.end())
{
pindexNew->pprev = miPrev->second;
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
}
// Chain trust
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
// Stake entropy bit
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
// Stake modifier (minimal for blocks far below checkpoint)
int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
if (pindexNew->nHeight >= nCheckpointHeight - 1000)
{
uint64_t nStakeModifier = 0;
bool fGeneratedStakeModifier = false;
ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier);
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
}
else
{
pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0);
}
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
// PoS stake seen set
if (pindexNew->IsProofOfStake())
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
// Insert into mapBlockIndex
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &mi->first;
// Link pnext for previous block
if (pindexNew->pprev)
pindexNew->pprev->pnext = pindexNew;
// Build tx index + UTXO entries, tracking money supply
int64_t nBlockValueIn = 0;
int64_t nBlockValueOut = 0;
int64_t nFees = 0;
unsigned int nTxPos = nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(block.vtx.size());
for (size_t nTxIdx = 0; nTxIdx < block.vtx.size(); nTxIdx++)
{
const CTransaction& tx = block.vtx[nTxIdx];
uint256 hashTx = tx.GetHash();
CDiskTxPos posThisTx(1, nBlockPos, nTxPos);
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
int64_t nTxValueOut = tx.GetValueOut();
nBlockValueOut += nTxValueOut;
// UTXO entries — read input values before erasing for money supply
if (!tx.IsCoinBase())
{
int64_t nTxValueIn = 0;
for (const CTxIn& txin : tx.vin)
{
CUtxoEntry utxo;
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
nTxValueIn += utxo.nValue;
if (fAddressIndex && !utxo.scriptPubKey.empty() && utxo.nValue != 0)
{
int nAType; uint160 aHash;
if (GetAddressFromScript(utxo.scriptPubKey, nAType, aHash))
{
txdb.EraseAddressUtxo(nAType, aHash, txin.prevout.hash, txin.prevout.n);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal -= utxo.nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
}
}
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
}
nBlockValueIn += nTxValueIn;
if (!tx.IsCoinStake())
nFees += nTxValueIn - nTxValueOut;
}
for (unsigned int k = 0; k < tx.vout.size(); k++)
{
if (!tx.vout[k].IsEmpty())
{
CUtxoEntry utxo;
utxo.nValue = tx.vout[k].nValue;
utxo.nHeight = pindexNew->nHeight;
utxo.scriptPubKey = tx.vout[k].scriptPubKey;
utxo.fCoinBase = tx.IsCoinBase();
utxo.fCoinStake = tx.IsCoinStake();
utxo.nTxTime = tx.nTime;
txdb.WriteUtxo(hashTx, k, utxo);
if (fAddressIndex && !tx.vout[k].scriptPubKey.empty() && tx.vout[k].nValue != 0)
{
int nAType; uint160 aHash;
if (GetAddressFromScript(tx.vout[k].scriptPubKey, nAType, aHash))
{
txdb.WriteAddressUtxo(nAType, aHash, hashTx, k,
tx.vout[k].nValue, pindexNew->nHeight, tx.vout[k].scriptPubKey);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal += tx.vout[k].nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
txdb.WriteAddressTxId(nAType, aHash, pindexNew->nHeight, (int)nTxIdx, hashTx);
}
}
}
}
}
// Money supply tracking — matches ConnectBlock formula
pindexNew->nMint = nBlockValueOut - nBlockValueIn + nFees;
pindexNew->nMoneySupply = (pindexNew->pprev ? pindexNew->pprev->nMoneySupply : 0) + nBlockValueOut - nBlockValueIn;
// Write block index to batch
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
// Update best chain
if (pindexNew->nChainTrust > nBestChainTrust)
{
hashBestChain = hash;
pindexBest = pindexNew;
pblockindexFBBHLast = nullptr;
nBestHeight = pindexNew->nHeight;
nBestChainTrust = pindexNew->nChainTrust;
nTimeBestReceived = GetTime();
}
// Set genesis block
if (pindexGenesisBlock == nullptr && pindexNew->nHeight == 0)
pindexGenesisBlock = pindexNew;
nLoaded++;
nPos += 4 + nSize;
// Batch commit every 200K blocks for LevelDB efficiency
if (nLoaded % 200000 == 0)
{
txdb.WriteHashBestChain(hashBestChain);
txdb.TxnCommit();
txdb.TxnBegin();
}
// Report progress every 5000 blocks to keep GUI responsive.
// AppInit2 runs on the GUI thread, so uiInterface.InitMessage
// triggers processEvents() which prevents the window from freezing.
if (nLoaded % 5000 == 0)
{
int pct = (nFileSize > 0) ? (int)((int64_t)nPos * 100 / nFileSize) : 0;
printf("FastImport: %d blocks indexed (%d%%)\n", nLoaded, pct);
uiInterface.InitMessage(strprintf(_("Importing blocks... %d indexed (%d%%)"), nLoaded, pct));
}
}
// Final commit
if (pindexBest)
{
if (fAddressIndex)
UpdateAddressIndexSyncState(txdb, pindexBest);
txdb.WriteHashBestChain(hashBestChain);
// Write sync checkpoint
Checkpoints::WriteSyncCheckpoint(hashBestChain);
}
txdb.TxnCommit();
}
nTransactionsUpdated++;
printf("FastImportBlockFile: indexed %d blocks in %" PRId64 "ms\n", nLoaded, GetTimeMillis() - nStart);
return nLoaded > 0;
}
string GetWarnings(string strFor)
{
string strStatusBar;
string strRPC;
if (GetBoolArg("-testsafemode"))
strRPC = "test";
// Misc warnings like out of disk space and clock is wrong
if (strMiscWarning != "")
strStatusBar = strMiscWarning;
// triangles: if detected invalid checkpoint enter safe mode
if (Checkpoints::hashInvalidCheckpoint != 0)
strStatusBar = strRPC = _("WARNING: Invalid checkpoint found! Displayed transactions may not be correct! You may need to upgrade, or notify developers.");
-1
View File
@@ -129,7 +129,6 @@ CBlockIndex* FindBlockByHeight(int nHeight);
bool ProcessMessages(CNode* pfrom);
bool SendMessages(CNode* pto, bool fSendTrickle);
bool LoadExternalBlockFile(FILE* fileIn);
bool FastImportBlockFile();
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
+104 -12
View File
@@ -42,20 +42,28 @@ bool DumpSnapshot(const fs::path& destPath,
return false;
}
// Collect block index entries (last nHeaders blocks, height ascending)
// v2: collect ALL block index entries (genesis → tip). Required so a
// snapshot-loaded node can address every block via mapBlockIndex +
// blk0001.dat. The nHeaders argument is honored only when strictly less
// than chain height for v1-compat diagnostic snapshots.
std::vector<std::pair<uint256, CDiskBlockIndex>> vHeaders;
vHeaders.reserve(nHeaders);
{
CBlockIndex* pindex = pindexBest;
unsigned int nCollected = 0;
while (pindex && nCollected < nHeaders) {
while (pindex) {
CDiskBlockIndex diskindex(pindex);
vHeaders.push_back({*pindex->phashBlock, diskindex});
pindex = pindex->pprev;
nCollected++;
}
// Reverse to height ascending order
// Reverse to height ascending order (genesis first)
std::reverse(vHeaders.begin(), vHeaders.end());
// Legacy v1 fallback: if caller passed a specific count smaller than
// the full chain, trim from the front (keep newest nHeaders).
if (nHeaders > 0 && nHeaders < (unsigned int)nBestHeight &&
vHeaders.size() > nHeaders) {
vHeaders.erase(vHeaders.begin(),
vHeaders.begin() + (vHeaders.size() - nHeaders));
}
}
// Open the chain DB once and reuse for both the UTXO count and the
@@ -91,6 +99,18 @@ bool DumpSnapshot(const fs::path& destPath,
int64_t moneySupply = pindexBest->nMoneySupply;
unsigned int numHeaders = (unsigned int)vHeaders.size();
unsigned int numUtxos = (unsigned int)nUtxoCount;
// v2: size of raw blk0001.dat content embedded in this snapshot. v1
// snapshots always write 0 here (no embedded blocks).
unsigned int numBlocks = 0;
{
FILE* blkFile = fopen((GetDataDir() / "blk0001.dat").string().c_str(), "rb");
if (blkFile) {
fseek(blkFile, 0, SEEK_END);
long blkSize = ftell(blkFile);
fclose(blkFile);
if (blkSize > 0) numBlocks = (unsigned int)blkSize;
}
}
uint256 contentHash; // placeholder, filled after writing data
fwrite(&magic, sizeof(magic), 1, file);
@@ -101,6 +121,7 @@ bool DumpSnapshot(const fs::path& destPath,
fwrite(&moneySupply, sizeof(moneySupply), 1, file);
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
fwrite(&numBlocks, sizeof(numBlocks), 1, file); // v2+
long contentHashPos = ftell(file);
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
@@ -182,6 +203,35 @@ bool DumpSnapshot(const fs::path& destPath,
}
}
// v2: After UTXOs, append raw blk0001.dat content. Streams in chunks;
// SHA256 covers the bytes. A snapshot-loaded node has full block data
// ready in datadir/blk0001.dat — no separate bootstrap needed.
if (numBlocks > 0) {
FILE* blkFile = fopen((GetDataDir() / "blk0001.dat").string().c_str(), "rb");
if (!blkFile) {
fclose(file);
strError = "Cannot open blk0001.dat for snapshot embedding";
return false;
}
printf("UtxoSnapshot: embedding blk0001.dat (%u bytes) into snapshot\n", numBlocks);
unsigned char blkBuf[64 * 1024];
size_t nLeft = numBlocks;
while (nLeft > 0) {
size_t nWant = nLeft > sizeof(blkBuf) ? sizeof(blkBuf) : nLeft;
size_t nRead = fread(blkBuf, 1, nWant, blkFile);
if (nRead != nWant) {
fclose(blkFile);
fclose(file);
strError = "Short read on blk0001.dat during snapshot embed";
return false;
}
fwrite(blkBuf, 1, nRead, file);
SHA256_Update(&sha256, blkBuf, nRead);
nLeft -= nRead;
}
fclose(blkFile);
}
// Finalize content hash and write it to the header
SHA256_Final((unsigned char*)&contentHash, &sha256);
fseek(file, contentHashPos, SEEK_SET);
@@ -189,8 +239,8 @@ bool DumpSnapshot(const fs::path& destPath,
fclose(file);
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, hash=%s)\n",
destPath.string().c_str(), numHeaders, numUtxos,
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, %u block bytes, hash=%s)\n",
destPath.string().c_str(), numHeaders, numUtxos, numBlocks,
contentHash.ToString().c_str());
return true;
@@ -240,7 +290,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
int height;
uint256 blockHash;
int64_t moneySupply;
unsigned int numHeaders, numUtxos;
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0;
uint256 expectedContentHash;
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
@@ -250,10 +300,22 @@ bool LoadSnapshot(const fs::path& snapshotPath,
fread(&blockHash, sizeof(blockHash), 1, file) != 1 ||
fread(&moneySupply, sizeof(moneySupply), 1, file) != 1 ||
fread(&numHeaders, sizeof(numHeaders), 1, file) != 1 ||
fread(&numUtxos, sizeof(numUtxos), 1, file) != 1 ||
fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
fread(&numUtxos, sizeof(numUtxos), 1, file) != 1) {
fclose(file);
strError = "Truncated snapshot header";
strError = "Truncated snapshot header (common fields)";
return false;
}
// v2+ has numBlocks between numUtxos and contentHash. v1 stops here.
if (version >= 2) {
if (fread(&numBlocks, sizeof(numBlocks), 1, file) != 1) {
fclose(file);
strError = "Truncated snapshot header (numBlocks)";
return false;
}
}
if (fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
fclose(file);
strError = "Truncated snapshot header (contentHash)";
return false;
}
@@ -460,6 +522,36 @@ bool LoadSnapshot(const fs::path& snapshotPath,
success = false;
}
// v2: After UTXOs, extract the raw blk0001.dat content. This makes the
// loaded node fully self-contained — no separate bootstrap needed.
if (success && version >= 2 && numBlocks > 0) {
printf("UtxoSnapshot: extracting %u block bytes to blk0001.dat...\n", numBlocks);
fs::path blkOut = GetDataDir() / "blk0001.dat";
FILE* blkOutFile = fopen(blkOut.string().c_str(), "wb");
if (!blkOutFile) {
success = false;
strError = "Cannot create blk0001.dat for snapshot extract: " + blkOut.string();
} else {
unsigned char blkBuf[64 * 1024];
size_t nLeft = numBlocks;
while (nLeft > 0 && success) {
size_t nWant = nLeft > sizeof(blkBuf) ? sizeof(blkBuf) : nLeft;
size_t nRead = fread(blkBuf, 1, nWant, file);
if (nRead != nWant) {
success = false;
strError = "Short read on snapshot blocks section";
break;
}
fwrite(blkBuf, 1, nRead, blkOutFile);
SHA256_Update(&sha256, blkBuf, nRead);
nLeft -= nRead;
}
fclose(blkOutFile);
if (success)
printf("UtxoSnapshot: wrote blk0001.dat (%u bytes)\n", numBlocks);
}
}
// Verify content hash
if (success) {
uint256 actualHash;
+1 -1
View File
@@ -11,7 +11,7 @@
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
// UTXO snapshot format version
static const unsigned int UTXO_SNAPSHOT_VERSION = 1;
static const unsigned int UTXO_SNAPSHOT_VERSION = 2;
// Number of block index entries to include in snapshot (covers difficulty,
// median time, stake modifier, and reorg depth requirements)