Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdb7253399 | |||
| d81a36f875 | |||
| f4f9c3b45a | |||
| 73c3cef8d4 | |||
| a38bfd2f97 | |||
| 23e8a2d647 | |||
| ad267866ab | |||
| 8c74f4e228 | |||
| f0e5dbdebc | |||
| 91d9233ea4 | |||
| 274aafab36 | |||
| 569b541931 | |||
| 600b1cf35f | |||
| 1d938d5770 | |||
| 8aeb5133bf |
@@ -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.
|
||||
@@ -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 "$@"
|
||||
@@ -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..."
|
||||
Executable
+691
@@ -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 "$@"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.");
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user