Compare commits

...

81 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
Sami Ahmed d73f6015a9 Merge feature/utxo-snapshot-auto-rebuild: signature auth + auto-rebuild + crash fixes
Adds:
- bootstrap: read manifest.json + verify file SHA256 (defense in depth)
- bootstrap: signature-based snapshot authentication (replaces checkpoint gate)
- checkpoints: drop 2207680 entry (signature is the gate now)
- init: auto-rebuild trigger (-autorerebuild=N) — wipe chain DB if stale
- init: remove FastImport as primary path (-allowfastimport, default off)
- utxosnapshot: set fSerializeChainTrust=true before LoadSnapshot writes
- init: skip block verification for snapshot-sourced chains
- init: don't fail on ResetSyncCheckpoint for snapshot-sourced chains
- build: ignore build-*/ directories

Server-side: utxo-snapshot.bin symlinked to utxo-snapshot-2207680.utx on bootstrap.cryptographic-triangles.org

End-to-end verified from zero: snapshot loads to height 2207680,
bestblockhash matches manifest, 4 peers connected via Tor.

Closes PR #8. Combines all the separate branches per Sami's directive.
2026-06-19 03:44:48 -07:00
Sami Ahmed ca16abe155 Merge v5.9.17-local-snapshot-trust: signed UTXO snapshot infrastructure
Adds the foundation for the snapshot-based IBD:
- sign-snapshot.sh: operator-side script to sign canonical snapshots
- utxosnapshot gate requireCheckpoint on trust source
- utxosnapshot build address index when loading (wallet balance support)
- main build address index during FastImport
- build: ignore build-*/ directories
2026-06-19 03:44:48 -07:00
Sami Ahmed be865c5944 Revert "utxosnapshot: v2 format — embed full blk0001.dat into snapshot"
This reverts commit 69529ea4c7.
2026-06-19 03:33:05 -07:00
Sami Ahmed 69529ea4c7 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 should be self-contained so a fresh node is fully
usable — can serve blocks to peers, fully verify the chain, validate
txs, and resume syncing forward. Replaces the legacy tri-bootstrap.tar.gz.

Format change (UTXO_SNAPSHOT_VERSION 1 → 2):

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

v2 HEADER:
  same + numBlocks (92 bytes)  ← new field

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 0 < nHeaders < chain
  height for v1-compat diagnostic snapshots.
- After writing 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 the UTXOs section, streams numBlocks bytes from the snapshot
  into dataDir/blk0001.dat.
- v1 snapshots (no numBlocks in header) still load via the partial
  path: headers + UTXOs only, no blk0001.dat written. The 'block
  verification skipped for snapshot-sourced chains' hack stays
  for v1, becomes unnecessary for v2.

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

This commit is format-only — signature verification, auto-rebuild,
and the LoadBlockIndex crash fix from PR #8 still apply unchanged.
2026-06-19 03:11:00 -07:00
Sami Ahmed dcfb650d9f init: don't fail on ResetSyncCheckpoint for snapshot-sourced chains
When LoadBlockIndex tries to reset the sync-checkpoint, it looks for
one of the known checkpoint blocks in mapBlockIndex and writes it to
the DB. For a freshly snapshot-loaded chain, mapBlockIndex only has
~1166 headers near the tip — none of the known sync checkpoints
(2205000, 2206004) are in that subset.

The reset returns false (no checkpoint found in main chain), and the
caller currently treats this as fatal: 'failed to reset sync-checkpoint'.
But for snapshot-sourced chains this is expected — the sync checkpoint
will be set when the node syncs past the next known checkpoint height.

Soften the failure: if fLoadedFromSnapshot is true, log a warning and
continue instead of erroring out.
2026-06-19 02:54:58 -07:00
Sami Ahmed 800f508abd init: skip block verification for snapshot-sourced chains
After LoadSnapshot, the daemon has headers + UTXOs but the raw block
bodies haven't been downloaded yet — they'll arrive via P2P as the
node syncs past the snapshot tip. LoadBlockIndex's verification
loop tries to read the last 50 block bodies from disk and fails
with 'OpenBlockFile failed' because the data isn't on disk yet.

Add fLoadedFromSnapshot global, set true at the end of successful
LoadSnapshot. In both txdb-leveldb.cpp and txdb-rocksdb.cpp LoadBlockIndex
verification loops, when ReadFromDisk fails AND fLoadedFromSnapshot is
true, log a warning and continue (the UTXO set itself was already
content-hash verified during LoadSnapshot, so we have strong evidence
the chain state is correct).

For non-snapshot chains (full blk0001.dat downloaded, normal IBD), the
ReadFromDisk failure remains a fatal error as before.

Combined with the prior fix in utxosnapshot.cpp that sets
fSerializeChainTrust=true before writes, the full snapshot path now
works end-to-end on a fresh datadir.
2026-06-19 02:47:30 -07:00
Sami Ahmed 78dae9fdaa utxosnapshot: set fSerializeChainTrust=true before LoadSnapshot writes
THE BUG: CDiskBlockIndex serialization is gated by a static flag
fSerializeChainTrust. LoadBlockIndex later sets this flag to true
based on dbformat >= 2 and tries to read nChainTrust as part of every
CDiskBlockIndex record.

But LoadSnapshot runs FIRST and writes CDiskBlockIndex records while
the static is still at its default value (false). The records are
written WITHOUT nChainTrust. Then LoadBlockIndex reads with flag=true,
expects nChainTrust, runs off the end of the buffer → 'CDataStream::read():
end of data: iostream error' → AppInit() exception.

This bug affected every fresh snapshot load: the snapshot's headers
and UTXOs loaded correctly (the per-record writes work), then the
post-load LoadBlockIndex crashed. Sami identified this as the
'format mismatch' blocker; the signature verification work went in
first but the underlying serialization bug remained.

Fix: explicitly set fSerializeChainTrust=true at the top of LoadSnapshot
before any CDiskBlockIndex writes. Then writes include nChainTrust.
Then LoadBlockIndex reads with the same flag set → matches.

The snapshot FILE format itself is unchanged — old snapshots produced
by daemons that wrote with flag=false will still fail to load (their
records don't have nChainTrust). New snapshots produced by daemons
that always write with flag=true (i.e. always include nChainTrust)
will load cleanly.
2026-06-19 02:42:09 -07:00
Sami Ahmed 48cf7277dd init: auto-rebuild trigger + remove FastImport as primary path
Two operational changes that together fulfill the 'snapshot as
universal sync start' vision:

1. -autorerebuild=<n> CLI flag (default 0=disabled)
   After Step 7 loads the chain DB, MaybeAutoRebuild() compares our
   local nBestHeight to the median peer-reported height (collected via
   CNode::nStartingHeight from the version handshake). If lag >= n,
   wipe the chain DB (preserve wallet.dat, onion, smsg state) and
   request shutdown. On restart, the daemon sees no chain DB and the
   snapshot path takes over.

   WaitForPeerHeights() polls up to 60s for at least 3 peers.

2. -allowfastimport CLI flag (default OFF)
   The FastImportBlockFile() rebuild path is now gated behind this
   flag. If the chain DB is empty and blk0001.dat exists, the daemon
   fails with a clear error message that tells the operator how to
   recover (place utxo-snapshot.bin, delete blk0001.dat, or set
   -allowfastimport). FastImport is now operator opt-in only — the
   snapshot path is the canonical sync start.

   This matches Sami's vision: 'Everything should be transferred over
   to the UTXO jump and then they should be able to put the blockchain
   together exactly how it's supposed to be from all the peers
   filling in all the blank spots.'
2026-06-19 02:36:09 -07:00
Sami Ahmed d6b47b5a0d checkpoints: drop 2207680 entry (signature is the snapshot gate now)
When I added the 2207680 checkpoint, I was treating checkpoints as the
authentication gate for snapshot loading. Sami corrected: 'It shouldn't
require a checkpoint, all it should require is a signature.'

Commit 2866a94 already replaced requireCheckpoint=true with signature
verification in DownloadUtxoSnapshot. This commit removes the now-
unnecessary checkpoint entry so the source stays clean — the signature
is the only gate for snapshots, period.

(2205000/2206004 checkpoints remain — they're separate concerns for
chain finality validation, not snapshot acceptance.)
2026-06-19 02:28:23 -07:00
Sami Ahmed 2866a94be1 bootstrap: signature-based snapshot authentication
DownloadUtxoSnapshot now authenticates snapshots via Triangles signed
messages instead of relying on hardcoded checkpoints.

New flow:
1. Fetch big manifest.json, find canonical snapshot entry
2. Fetch the per-snapshot manifest (utxo-snapshot-{h}.manifest.json)
3. Verify the signer address is in the trusted signers list (currently
   Sami's TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX)
4. Verify the signature cryptographically (Triangles compact-message
   protocol with strMessageMagic prefix, same construction as
   signmessage/verifymessage RPC)
5. Download snapshot file, verify SHA256 against manifest
6. Load with requireCheckpoint=false — signature is the gate

Per Sami: 'It shouldn't require a checkpoint all it should require
is a signature.' This removes the checkpoint coupling that was
breaking fresh-node sync (the 2207680 checkpoint gate rejected the
canonical snapshot even though it was validly signed).

Trusted signer list is currently a hardcoded constant. Future work:
-snapshotsigner=<addr> CLI arg (repeatable).
2026-06-19 02:24:07 -07:00
Sami Ahmed 2a7c89a91e bootstrap: read manifest.json for canonical snapshot + verify SHA256
DownloadUtxoSnapshot now:
1. Fetches manifest.json from the bootstrap server
2. Locates the utxo_snapshot entry (filename + expected sha256)
3. Downloads THAT file
4. Verifies file SHA256 matches manifest
5. Falls back to legacy 'utxo-snapshot.bin' if manifest unavailable

Also add 2207680 checkpoint to mapCheckpoints so the canonical signed
snapshot (per 2026-06-18 manifest) passes the requireCheckpoint gate.

Defense in depth: server symlinks + daemon verifies the file matches.
2026-06-19 02:02:53 -07:00
Sami Ahmed 677a8ea79a build: ignore build-*/ directories and build artifacts 2026-06-19 00:17:10 -07:00
SamiAhmed7777 b40c58f886 Add triangles-cli: JSON-RPC client (port bitcoin-cli pattern) (#7)
* 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.

* 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.

* 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)

* 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.

* 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.

* 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.

* diagnostic: add tracing to Windows packaging step

* 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.

* 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.

---------

Co-authored-by: Krystie <krystie@sami>
2026-06-18 20:05:54 -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
hermes d8af2aa17c scripts: add sign-snapshot.sh for signed UTXO snapshot provenance
Generates a UTXO snapshot via dumputxoset RPC, signs a provenance message
(height, blockhash, snapshot sha256) with signmessage, and emits a signed
manifest.json. Verification via ./sign-snapshot.sh verify <manifest> <snap>
or verifymessage RPC on any node.

Pairs with the requireCheckpoint trust-gate patch — local snapshots no
longer require a known checkpoint, so signing provenance is the way to
establish authority for a snapshot.
2026-06-18 02:39:43 -07:00
hermes e15de97be3 utxosnapshot: gate requireCheckpoint on trust source
Local file snapshots (init.cpp) skip the known-checkpoint gate; P2P-delivered
snapshots (bootstrap.cpp) keep it. Rationale: the checkpoint gate exists to
prevent malicious peers from injecting fake UTXO sets. Local file loads come
from operator-trusted sources (filesystem access already grants equal power),
so the gate is unnecessary friction.
2026-06-18 02:34:51 -07:00
triangles-bot c606253c41 utxosnapshot: build address index when loading a UTXO snapshot (fast-start nodes get balances) [v5.9.17] 2026-06-16 20:37:44 -07:00
triangles-bot b2dfb627cc main: build address index during FastImport (fix-in-place, v5.9.16) 2026-06-16 20:24:26 -07:00
triangles-bot d0a76f8ae2 qt: show Seed Phrase (HD Backup) in the visible Operations menu (v5.9.15)
The HD seed action was only added to the standard Qt menu bar, which the
skinned GUI hides. Add it to menuOperationsRequested() so users can actually
reach Generate / Reveal-for-backup / Restore from the Operations menu.
2026-06-16 16:24:12 -07:00
SamiAhmed7777 cc57c906b4 Merge PR #6: HD seed-phrase wallet + fast-sync checkpoint/snapshot (v5.9.14)
HD wallet (BIP39/BIP32 seed phrases) - daemon + Qt
2026-06-15 20:44:52 -07:00
Sami e80d672833 checkpoints: add 2206004 checkpoint + UTXO snapshot hash (fast new-node sync) 2026-06-15 20:31:02 -07:00
Sami fcdc9a58b0 ci(lint): checkout secp256k1 submodule for clang-tidy (fixes configure) 2026-06-15 19:26:43 -07:00
Sami 514867c5d9 wallet(HD): flush keypool on seed set so getnewaddress yields HD keys immediately 2026-06-15 19:16:03 -07:00
Sami c464e6c59d wallet(HD): Qt UI - Seed Phrase dialog (generate/restore/backup)
Adds HDSeedDialog (Settings > Seed Phrase) with Generate New / Reveal for Backup / Restore from Phrase, driven by new WalletModel HD methods. Restore rescans the chain. Requires wallet unlock via the standard UnlockContext.
2026-06-15 19:16:03 -07:00
Sami 11ed086d1e wallet(HD): native BIP39/BIP32 HD wallet - daemon side
Adds deterministic HD key derivation (path m/44'/2222'/0'/0/i, matching the TRIdock web wallet) wired into CWallet: HD seed stored in wallet.dat (encrypted with the wallet master key when the wallet is encrypted), keypool derived from the seed, and new RPC commands hdnew/hdrestore/hdshow/hdinfo. Crypto core verified standalone against the official BIP39 vector and triWallet.js addresses.
2026-06-15 19:15:43 -07:00
Hermes 5511cfae6b v5.9.14 + pitfall #61 guard: initialize pindexFinalized on startup
ROOT CAUSE of the 2026-06-16 minority-fork reorg:

The v5.9.14 getheaders handler serves headers from pindexGenesisBlock
when a fork peer sends a locator that doesn't match our main chain. The
intent was to help fork peers learn the canonical chain. The bug: this
allows the fork peer to feed us THEIR short chain back via getheaders,
and we accept it because:

1. pindexFinalized is NULL on a fresh restart (the auto-checkpoint code
   in ActivateBestChain() at main.cpp:2459 only sets it when
   !IsInitialBlockDownload(), but a synced daemon restarting with a
   chain tip > 24h stale is considered IBD by the time check at
   main.cpp:1331).

2. With pindexFinalized = NULL, the reorg guard at main.cpp:2198
   short-circuits: 'if (pindexFinalized && pfork->nHeight < ...)'.

3. The fork peer's 3,755-block chain gets accepted, overwriting our
   healthy 2,206,004-block chain.

THE FIX (two parts):

A. init.cpp:1083-1113 — after LoadBlockIndex(), call
   Checkpoints::GetLastCheckpoint(mapBlockIndex) to initialize
   pindexFinalized from the hardcoded checkpoint (block 2,205,000).
   This is a no-op on the first ~10 seconds of the daemon's life
   (during the initial IBD walk), but as soon as we sync past block
   2,205,000, the checkpoint is in mapBlockIndex and pindexFinalized
   is set for the daemon's entire lifetime.

B. main.cpp:4473-4496 — in the getheaders fork-detection branch,
   serve from pindexFinalized->pnext (the block after our last
   finalized checkpoint) instead of pindexGenesisBlock. This is the
   safe equivalent of the v5.9.14 'serve from genesis' logic — the
   peer learns our canonical chain from the most recently finalized
   point forward, and their short fork gets rejected at the reorg
   guard in Reorganize() because the fork point is below
   pindexFinalized.

Combined: a fork peer can no longer drag us below block 2,205,000
because (a) pindexFinalized is always set on a synced restart, and
(b) the reorg guard now sees a non-NULL pindexFinalized and rejects
any fork below it.

Tested manually by simulating a restart with the v5.9.14 binary on
a chain that had been reorged to a minority fork; the new build
refuses the reorg and prints 'STARTUP-CHECKPOINT: pindexFinalized set
to block 2205000' on startup, then 'getheaders: fork detected from
peer ... serving headers from finalized block 2205000' on the first
fork peer's getheaders request.
2026-06-15 18:37:53 -07:00
Sami Ahmed 1dda8b3006 Auto-repair corrupt Tor state file on daemon startup (pitfall #19)
Tor's atomic state-write is: write state.tmp, then rename to state.
If the daemon is killed mid-write (pkill -9, OOM, power loss, disk-full),
the rename can fail and 'state' is left as a regular file instead of a
directory. On next start, Tor's config validator refuses to use it:

  [warn] State file '...' is not a file? Failing.
  [err] set_options: Bug: Acting on config options left us in a broken state. Dying.
  [err] Reading config failed--see warnings above.

The daemon then reports 'Tor failed to start. Triangles requires Tor to
operate.' (the error from the 2026-06-15 TRI-LAPTOP GUI wallet
screenshot) and refuses to come up at all.

Hit before on DNS3 (2026-05-24, fixed by manual 'mv state state.file.bak;
mkdir state' on the operator) and on the laptop today. The fix was always
the same one-liner; this patch makes the daemon do it itself.

Behavior:
- Detect: if tor_data/state exists and is NOT a directory, it's corrupt
- Quarantine: rename to tor_data/state.corrupt-YYYYMMDD-HHMMSS for
  inspection (the user might want to recover the file's contents)
- If rename fails (Windows anti-virus holds the file, etc.), retry with
  remove-then-rename, and as a last resort just remove() so Tor can
  proceed
- Print a clear 'Tor state was a file (corrupt) — quarantined to ...'
  log line so the operator can see it happened
- Tor then recreates state/ as a fresh directory and bootstraps normally

This is a pure-additive change (no existing behavior modified). Builds
clean with the existing C++20 + libtor.a toolchain. No version bump
needed - will roll into the next CI cycle.
2026-06-15 16:29:05 -07:00
Sami Ahmed 68c4e38411 Fix getheaders pnext null bug: serve main chain from genesis, tip-backwards fallback
Two related bugs in the getheaders handler at main.cpp:4441:

1. Locator-mismatch returns 0 headers:
   GetBlockIndex() falls through to pindexGenesisBlock when no locator
   hash matches the main chain. pindexGenesisBlock->pnext is null, so
   the for-loop exits immediately and the peer gets an empty headers
   response. This was the DNS3 headers-first sync stall (pitfall #36):
   'getheaders -1 to 0000...' logged repeatedly.

   Mirror the getblocks handler (line 4387): detect the mismatch, log
   a warning, and serve headers from genesis so the peer can discover
   the canonical chain.

2. Broken pnext chain at any height:
   Even when GetBlockIndex() returns a valid (non-genesis) block, its
   pnext can be null — this happens when LoadBlockIndex() didn't fully
   heal pnext links (e.g., the node was bootstrapped from a snapshot,
   or the chain was interrupted by a crash). On tridock every pnext
   link was null despite 2.2M blocks (the June 11 'Heal pnext links'
   commit addressed a similar symptom for GetKernelStakeModifier() but
   doesn't reach into the getheaders handler).

   Fall back to a tip-backwards walk from pindexBest when pnext is
   null: O(N) but correct, and only triggers on the broken path.

   (References the design in references/getheaders-pnext-fix.md from
   the v5.9.10 deploy notes — that fix was never actually committed,
   only prototyped and dropped during the June 4 git cleanup.)

Bump to v5.9.14 (also embeds the embedded-Tor 0700 fix from ed996c8).

Refs: SKILL.md pitfall #36
2026-06-15 15:43:03 -07:00
Sami Ahmed ed996c8e9d Fix embedded Tor bootstrap (code -1): force 0700 on hidden service dir
Tor's config validator refuses to start a hidden service on any directory
whose permissions are not 0700. The daemon's fs::create_directories() honors
the process umask (0022 on Linux), leaving hidden_service/ at 0755. tor_run_main()
returned -1 with:

  [warn] Permissions on directory .../hidden_service are too permissive.
  [warn] Failed to parse/validate config: Failed to configure rendezvous options.

This was misdiagnosed as a libtor.a build problem (the June 4 rebuild was a
red herring). The real fix is two fs::permissions() calls after create_directories().

Reproduced with a standalone harness linking libtor.a, fixed, SOCKS port 19099
came up in 1 second and Tor began bootstrapping normally.

Also force 0700 on the DataDirectory itself - same validator, same rule.

Refs: SKILL.md pitfall #59
2026-06-15 15:32:28 -07:00
Sami Ahmed cd9e023865 Bump version to v5.9.13 (transparent Qt wallet icon) 2026-06-13 20:49:29 -07:00
Sami Ahmed f273d651ed Make Qt wallet icon transparent (remove white field around triangle) 2026-06-13 20:45:23 -07:00
Sami Ahmed 2ac88b4e0a Add finality checkpoint at 2205000 (anti-fork); v5.9.12 2026-06-13 15:09:44 -07:00
sami7777 c1340441cc Bump version to v5.9.11 2026-06-11 02:57:42 -07:00
sami7777 c99d859781 Fix use-after-move crash in orphan block handling; add -ignoredupstake recovery flag
mapOrphanBlocksByPrev.insert dereferenced pblock2 after std::move'ing it into mapOrphanBlocks - guaranteed null deref (segfault at offset 4) on every orphan block received. Capture hashPrevBlock and the raw pointer before the move.

Also add -ignoredupstake (default off): bypasses the duplicate proof-of-stake rejection so canonical blocks can be imported when a fork twin staking the same outpoint was seen first. Recovery/diagnostic use only.
2026-06-11 01:34:34 -07:00
sami7777 713f69e137 Gate 7-day coin-age soft cap behind activation timestamp
The soft cap (1c068f4, 2026-04-20) shipped without a height/time gate, retroactively invalidating blocks staked earlier with long-aged coins (e.g. coins idle through the 2022-2026 freeze; block f9f976d0 at 2203410 stakes a 3.4-year-old coin). Apply the cap only to stakes at/after 1776000000 (2026-04-12 ~13:20 UTC); historical stakes validate under the rules they were created with.
2026-06-11 01:34:21 -07:00
sami7777 e3f397c7e5 Heal pnext links on active chain at LoadBlockIndex
Persisted hashNext can be stale or zeroed by crash-interrupted reorgs, breaking GetKernelStakeModifier()'s forward walk and silently rejecting valid new PoS blocks ('check kernel failed', hashProof=0). On DNS3 every one of 2,201,458 links was zeroed on disk. Root cause of the 2026-04-24 chain halt. Rebuild the in-memory links from pindexBest at every startup.
2026-06-11 01:34:20 -07:00
Krystie f80fb98f68 Merge master into cpp20-modernization (RPC fix, auto-bootstrap, tor_data fix)
Brings in:
- 79b0c4a: Fix RPC thread crash on bad auth (T001) — adapted to C++20 style
- d0fb2dc: Enable auto-bootstrap for GUI wallets
- 89a480a: Fix tor_data/state directory trap + make -notor work
- 372b252: Fix Windows CI shell for git submodule
- 6c56e41: Init secp256k1 submodule before build

Kept v5.9.9 version (cpp20-modernization's, newer than master's v5.9.7).
CI workflow kept cpp20-modernization's version (submodule init already present).
2026-06-04 18:25:12 -07:00
Krystie 610b61b1b1 Fix chain reorg crash: coinbase timestamp check + corrupted checkpoint hash
CheckBlock() at line 2825 used the heightless FutureDrift() overload,
which hardcodes 90-second drift (post-FORK_HEIGHT_V5_4). During reorgs
from the block-570 fork chain, block 571 (July 2014) has a coinbase
timestamp that legitimately exceeds 90s before block time, causing:
  ERROR: CheckBlock() : coinbase timestamp is too early
  ERROR: Reorganize() : ConnectBlock failed
  -> infinite crash loop (791 iterations recorded)

Root cause: commit a671708 (v5.8.3) tightened drift from 3min to 90sec.
The heightless overload applies this to ALL blocks regardless of height.

Fix: use explicit 10-minute tolerance in CheckBlock (context-free, no
nHeight available). The tight 90-second check is still enforced in
AcceptBlock/ConnectBlock with proper height context.

Also fix corrupted checkpoint hash at block 3935: truncated '07' in
both mainnet and testnet tables during C++20 modernization.
2026-06-04 18:21:43 -07:00
Krystie e8edcd4aa6 Merge remote-tracking branch 'gitea/master' 2026-05-30 23:01:53 -07:00
Krystie 3928f86657 Merge remote-tracking branch 'gitea/master' into cpp20-modernization 2026-05-30 23:00:56 -07:00
Krystie 67d838433d Fix getblocks: serve main chain to fork peers instead of banning them
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / test-linux-sanitizers (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
Fork nodes (DNS3/DNS2 stuck on block 570 chain) send locators that
don't match any main chain block. Instead of disconnecting/ banning
after 3 failed getblocks attempts, this change:

- Serves main chain blocks from genesis when locator has no match
- Resets nIncompatibleGetblocks counter after 10 (so we never ban)
- Fork nodes will receive, validate, and automatically reorg to the
  longer/higher-work main chain once they see it

This fixes the 'no common blocks' deadlock while preserving chain
integrity — only a genuinely longer chain can trigger the reorg.
2026-05-24 22:36:13 -07:00
Krystie e3aeff002c Remove block 570 checkpoint - let nodes sync naturally to main chain
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / test-linux-sanitizers (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
2026-05-24 21:54:42 -07:00
Krystie 09ccd4339c IBD stall fix: restore IsInitialBlockDownload() time check, add block 570 checkpoint, fix version handler IBD bypass 2026-05-24 15:40:29 -07:00
sami7777 03aa38f1b4 Promote NewIterator() override to public in both chain DB backends
The base class CTxDBBase declares NewIterator() public, but both
backends overrode it in their protected: section. That narrowed the
static access through the derived type, so the migration utility
(which holds concrete CTxDB / CRocksTxDB instances) couldn't call it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:01:44 -07:00
sami7777 9389a883f1 Wire CSyncManager + LevelDB->RocksDB chain DB migration
CSyncManager extracts the headers-first IBD planner from main.cpp into
its own translation unit. main.cpp loses ~570 lines of file-scope state
and helper functions; the headers handler, block-delivery latency
tracking, stall-recovery, and per-peer Tick cadence now route through
g_syncManager.

MaybeMigrateLevelDbToRocksDb() is now reachable via -migratechaindb /
-migratechaindbforce in init.cpp. Reads from <datadir>/txleveldb and
writes byte-for-byte identical records into <datadir>/rocksdb via a
new CRocksTxDB::WriteRawRecordForMigration() shim over WriteRaw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:44:08 -07:00
sami7777 31fa26f03a Drop checkpoints > 2,000,000
Trim mainnet and testnet checkpoint tables to the 2,000,000 entry.
Clears the snapshot-hash entry at 2,203,594 since its corresponding
checkpoint is now gone (per the invariant noted in the comment block).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:07:58 -07:00
sami7777 ce96d278cd Bump client version to v6.0.0
Internal refactor milestone for the C++20 modernization series.
No protocol or on-disk format change (version.h untouched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:07:57 -07:00
Krystie 7166b76bad ci: fix test-linux-sanitizers submodule checkout 2026-05-10 19:36:16 -07:00
Krystie 540db0e210 Fix wallet logging and Qt min-fee enum regressions 2026-05-10 19:18:47 -07:00
Krystie 59b75476ca ci: autonomous fix iteration 2
Generated by triangles-ci-loop.sh on 2026-05-10 18:20:35 PDT. See /var/log/triangles-ci-loop.log.
2026-05-10 18:20:35 -07:00
Krystie 0029b34698 ci: autonomous fix iteration 1
Generated by triangles-ci-loop.sh on 2026-05-10 18:02:40 PDT. See /var/log/triangles-ci-loop.log.
2026-05-10 18:02:40 -07:00
Sami 8e03e89764 ci: trigger Build All Platforms on push to cpp20-modernization 2026-05-10 17:47:35 -07:00
Krystie 3b1850af9d ci: autonomous fix iteration 1
Generated by triangles-ci-loop.sh on 2026-05-10 12:24:48 PDT. See /var/log/triangles-ci-loop.log.
2026-05-10 12:24:48 -07:00
Krystie 372b252294 Fix Windows CI: use bash shell for git submodule commands
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / test-linux-sanitizers (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
On Windows MSYS2 runners, the default shell is 'msys2' which doesn't
understand 'git submodule' commands the same way. Adding shell: bash
forces the step to use bash, which properly executes git and finds
submodule content.

Affected jobs: build-windows-qt, build-windows-daemon
2026-04-30 01:27:08 -07:00
Krystie 6c56e41e82 Fix CI: init secp256k1 submodule before build
The secp256k1 submodule (src/secp256k1/) was not being checked out
by the default shallow checkout, causing CMake to fail with:
  'src/secp256k1 is empty. Run: git submodule update --init --recursive'

All 6 build jobs (Linux unit/sanitizer, Windows Qt/daemon, Linux Qt/daemon,
macOS) now:
1. Use fetch-depth:0 to get full git history (needed for submodules)
2. Run 'git submodule update --init --recursive' after checkout
3. Proceed with the normal build steps
2026-04-30 01:08:27 -07:00
Krystie 79b0c4a176 Fix RPC thread crash on bad auth (T001)
- HTTPAuthorized: validate strAuth length before substr(6), wrap DecodeBase64 in try-catch
- RPCAcceptHandler: wrap body in try-catch to ensure counter decrement and conn cleanup
- ThreadRPCServer3: wrap while loop in try-catch for graceful exception handling

Bad auth attempts now return HTTP 401 without killing the RPC listener.
2026-04-29 19:30:13 -07:00
Krystie 3db537d759 Update T012 status: design complete 2026-04-29 19:19:35 -07:00
Krystie 63be053b1d Add TRI v6 autonomous development task queue 2026-04-29 19:06:22 -07:00
Krystie d0fb2dc105 Enable auto-bootstrap for GUI (Windows) wallets
Previously the bootstrap auto-download was guarded by #ifndef QT_GUI,
meaning the Windows Qt wallet would never auto-bootstrap on fresh installs.
This left GUI users stuck at block ~570 during IBD with no way to recover.

Now both GUI and daemon builds automatically download bootstrap data from
bootstrap.cryptographic-triangles.org when no blockchain data is found.
Progress is shown in the GUI status bar via uiInterface.InitMessage.
2026-04-29 17:18:44 -07:00
Krystie bea3c4447c Bump version to v5.9.7.0 2026-04-29 16:48:16 -07:00
Krystie 89a480a85a Fix tor_data/state directory trap + make -notor actually work
1. tor_process.cpp: Auto-recover legacy 'state' subdirectory
   - Old builds created tor_data/state/ as a directory and set
     DataDirectory to point at it. Tor 0.4.9+ rejects this because
     it expects to write a 'state' FILE inside DataDirectory.
   - Fix: Point DataDirectory at tor_data/ itself. On startup,
     if a legacy 'state/' directory exists, migrate contents up
     and remove it.

2. init.cpp: Allow -notor to actually bypass Tor requirement
   - Previously, -notor made StartEmbeddedTor() return false,
     which hit the 'Tor failed to start' error path and killed
     the wallet. Now -notor enables clearnet-only mode for
     diagnostics, benchmarking, and recovery.
   - Updated help text to reflect actual behavior.
2026-04-29 16:39:59 -07:00
Krystie b4308e42ad Smoke-test the Krystie loop runner
Krystie Gate / Static gate (red-list / test-first / no-clearnet) (push) Successful in 35s
Krystie Gate / Build + ctest (push) Successful in 7m11s
Krystie Gate / Auto-merge to master (push) Successful in 28s
This issue was created to exercise the autonomous runner end-to-end.

**Acceptance:** runner picks this up, demo worker appends a line to docs/krystie-runner-log.md, branch krystie-wip/triangles_v5-1 is pushed, gate fast-forwards to master, this issue auto-closes.

Closes #1
Refs: krystie-wip/triangles_v5-1
2026-04-28 23:57:31 -07:00
Sami c4656ac244 fix: gate auto-merge — use git push instead of PATCH /branches/master
Gitea PATCH /repos/{owner}/{repo}/branches/{branch} is for renaming branches, not for moving refs; it always returned failure even when master had not diverged. Replace with a plain git push (token in extra header) which fast-forwards iff the update is FF-clean — same safety, correct mechanism.
2026-04-28 23:56:51 -07:00
sami7777 2da1c039a8 Gate: accept Krystie subkey ID (git %GK returns subkey not primary) 2026-04-28 22:42:22 -07:00
sami7777 2b701f6640 Gate: handle force-push orphaned-history (fall back to head-only inspection) 2026-04-28 22:39:14 -07:00
sami7777 de4498b8eb Fix gate: trust Krystie public key after import so %GK verifies 2026-04-28 22:35:51 -07:00
sami7777 815cc02aa9 Bootstrap Krystie autonomous gate (manual one-time setup)
Adds the gate workflow + check script + Krystie public key under .gitea/.
This commit is intentionally unsigned so the gate treats it as an admin
bootstrap rather than a Krystie commit (which the gate would otherwise
require to land via krystie-wip/* + auto-merge).

After this, master branch protection will be enabled requiring the
Krystie Gate workflow to pass on all future pushes. Krystie will push
to krystie-wip/<task-id> branches and the workflow auto-merges on green.

See: krystie-buildout/workflows/* in the krystie repo for sources.
2026-04-28 21:42:57 -07:00
65 changed files with 5841 additions and 1218 deletions
+65
View File
@@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGnxdoUBEACaICSRk5Clg4kI5IubMXnXLbsSWzi0TKIpqh4Tqgl2k1bgSxda
tuBabHcsaw6Kpo96CJl9aZ63VIrEhCSdirGm/wWlbnTvm6cK4EDucGgS4BdEfm9B
Lw2c+iTjuJqJt2HLbRkZmF8qHy0Mo1DjsjbWUiwIP62RkuxCNuW2Wl9euak504UW
ZTFB9f3Bu1C6rknsWQ0VR5HJwWN4UrVMukZhvlzLRjKgW7W2XchSXUIAe7b0/5jo
pFB30pwxbaBIoeJu8AHYnzBYRThp0WbDTC/LK5FSnSgG751jOtkbheRNGjO65a2L
gkaclxo1NUIIu+WqdBtTbpUQM7UEd50FOXxUgq/xJhGujNMJyOMMPEzfJ+kP9pD4
p+gkNCLLgvT+gu1PnF0iTIAb4qggHGzZGRgc5lTxC28XEud0DAx+Pdcdf/nlQTsu
AOjZZgiiLIjwJZo/RYwId1Wh+LmtYZqVZ6j4vqqaXXPADpN40LGyUo376+oVSn77
1w2j1CWSmTEPaq4KmvTvnTvFfbeXkKckmUziBYwqZI0uA2xE6ShNUaAS4kdIaZhO
Bb3t9xrwu2QAR1rRlNTCChOyNbauvo32GLRnXg5BXYTBsmMU/QHe6EBJsycq/IHl
2yNPQUtynxzkDZ9OYrwbZaTZOCJK0pHwm4HUmV3rPiEPXUKJXXDojWQYpwARAQAB
tHlLcnlzdGllIFRyaWFuZ2xlcyBSZWxlYXNlIChBdXRvbm9tb3VzIHJlbGVhc2Ug
c2lnbmluZyBrZXkgZm9yIHRyaWFuZ2xlc192NSkgPGtyeXN0aWUtdHJpYW5nbGVz
LXJlbGVhc2VAZG5zMi5zYW1pLnRhaWxuZXQ+iQJYBBMBCgBCFiEEUjqBgz63IBVz
4e/h3PJXmWgQeYQFAmnxdoUDGy8EBQkDwmcABQsJCAcCAiICBhUKCQgLAgQWAgMB
Ah4HAheAAAoJENzyV5loEHmEPm0P/3y2Y5Y1rhgSj6yN/1PuXhpp1sNqXBOJZxTW
uUx/4LUqLgqbtFC0fR4BwpTYEkGGaofi0/95sPwKu0jmVR6hJ+8Omk/4TMRmXUYq
JUTA0/xzj9sOndaqiwRY3Y/YO/ytahL89y8xl5cYSaOOwLI/f9xo8pq1t20Iiuiw
kcaUBRQgpTVMI49VcXwrEUMnjV9cldGqql8v7CSKds5rRxQgT8ifaC6euTWxK0Tn
5Yu/wnBd+akU5/bcI8PEp5VyUyAJMZJPZ6mUqriWXlnhiUj0NawEKtfG9qlkMixL
5ujz9lu/9MvFUYC4QSvcd1O3k9MJ6T4Yk/uEygEca8Y/3DcccWRMHjW2Ah+ewhHE
yHy0tctzCe7pco+jfB7zicKv0bjXarvwBZ43e5F/zG5PMpo0XAS9EkEUV+/9BJ38
jBHvzqwXsYTnxS0hgOSONJk9Cc6i0NN1ex3rPOrYvBvHWZ+9n3AU2taUljuypDGO
RweCHsFMYGx/oOI94bD7wTeVey0tAZ+3Urz6T5qY5SmNKiwZ5NtbYo0Mp8r5DdPJ
N9KtXtaDMPI/rORjl1Ad9xhDbGMCr7EH9SjTU+z51me31/ZU58jICGlvm3/JDcb5
CAWyDppvW0ul9yqo1fecSi3w7m2sI+4F+tj8oLFmO+5rQw85F4LPqjVVMbUUkoAH
udtoU3Y8uQINBGnxdoUBEACtFpgwuwEZqxbsfmL+uBxHnxSSRm2vlQc7HRtQG6Nu
Tg1x4s9xFO6kNkcslPgZx9XSvFkPt1RUCNViTYE34UoOfkBs+aNkw4ztwuKGt/AS
CZFRX99yBx7P0kiV4Nt/Cj3oQBtEXQixMmGK4+N0WBskV/QxRFA7hl+ZQBeEFsYP
15UyjX2h6HFRYTSPKufEmtE/OkO9dg3fyxTvZ3+1o3eWWjT4VReX4jvmzXn3RNP1
BwuAy+iwmnqUBcuEZ0qQiT/+oRLCHOFLCAjVoSsPY9WJfF67XpDb2noV/0RqltMD
jUc/MT8Bxn/y8qHKvQuyPms/YO5jMI7q+/D1eayO4R48qhsMVp6Rjb31xalMWT2W
rwQg1XaFG80vUisbfX6CU0sH34tWQkqAL7AiwradPtwB0Sn60Em5UgHdWQ7rkd+h
mFOUjYi3Q1hOuPQNuzDK51n5sv8qOIrfghR0F2AtRkpbhBYM9435U+JkcZTjJ6wp
WYLBTAys4qo9MnL18Z4byaw4e122eBgI3/UOvG+7C7wIAwmiDvnYzqErz7iOmuTe
+cgdWYmLFvkfx8P6Ka+6likSV4ZY/ASP4Uo/gTspatwqHApAmphfVEGwm0/wKMl2
Br+zuZZ8RJ1GxahwJ1oo3uuGjIQjGNplh2wHVvbsfg4mlFKDbShdJ5adtx/E6BrT
NQARAQABiQRyBBgBCgAmFiEEUjqBgz63IBVz4e/h3PJXmWgQeYQFAmnxdoUCGy4F
CQPCZwACQAkQ3PJXmWgQeYTBdCAEGQEKAB0WIQRpE+E2EPaYGDQpziDC3GBhjIWh
WQUCafF2hQAKCRDC3GBhjIWhWQYID/0Ru2U9rLatIAjoSWI6TMFaOaxHf1NAsTcz
fPRbFNxx0d4ByjfjLlrfnDpQXsFpMa6/BpQ1Ps1ApW+wQsuHXxj/jdZVSi5f/sOT
XKZq/MRZu8enA1foj0b6sJ13ZWY0iIWmIeK8NWuNBFWz2QTjRie2hqoOTR+Hy43r
gRMlzPaXNoeD2UuvhoDphH2g2OWcppxd2b1yk7W9kh0CgvXXg4cPee71LmXLZMoL
GJcmtSkU24fiwa95TSk2J5qQ3voP5Knk8e/VgGmOSUoUzr+O5N6tEO2KPVr3bsFt
8zKHEyuddDYUju4U2Fl+xq4yJCYX3h6AKyh/c3bOAGp4f3zs62XPjn9RIXlTH9Lw
Vp97pJRzAEYzXRGXfGJRz54hQzft1L+BkhqWpVwzxI1fnflpVghahHOIoa0bnpyH
ycxxvkGY6o5TS5Ymqf4yry/4G+C64kX2GlBgmN2I2+UJ3z/cyEqY4XVMGk4S7uLq
d0eKrA2ZaSHUce0F/gGpMynxGFP+BNlfNBcSwzgBbnvcyFhOtls4LvTAcLmyBpjM
gEugtkskDSxJd/HcnTcFF5P9UcVPdD7vg7tlUXQ37AvbeppFC4pFbxYK01SOYk+W
nXH/Mq1XkFFcArVtsL1octAWuaqn8M/5kXnKvhw/TCBNPfQ7Kljx1V65kErMXNl2
F/cJXWQKCXPtD/92EXa9uvIxCINwxyZidwEvqx1xpBTIDDdYvDt8ZXHr957xpiaz
ls3aHy0mMUGigzVEL0AcPToBEudEzy+z1pB0y23znveycDZRTRsGnDwLrdb9eqTu
JDViRtB6WBASGsU3XHMYFietvEukmqJj55KCDl5YapZDKUb1iraERJ72PH9xk3C7
501Cklfe+GM8VBymwApOjWPLw1cIxVOL/Ex9ADsVMYDubAVh0LnqvDTg8e8bv4gu
BhyC2AXsQIUZ9HtixfvLZ6sdsPjstlQj+ZinpTHWthx52jrfcRYOo32cE06BpR3U
bQ+mjn6orzZ7Iq5p6aejukCddvlSX381vMaLf1/FGzmu/9f52p7uTLxU7N8sEcqq
PlkdRYatwWDeKuGpYVqmXuPvAaPD/sfH6zw0O5JjcNhb5KqTMjcV7IXV+V7QU2F5
iH5eYepAFf5uctffFMlCZ2YtCLlISMxHWLLqupIlu/JumTLcUjXUpOMV/sp+v6gD
66yx5QQWtVdYT9dYW+EUybjuWlS85T9DJVrPx5GiQfKjgFzuyuEvsbExzVBOwsBP
o/pPUWyBNSI6YVrm329U7ybAuDdnTveaMtIxRneN8mM9lhXNWpb8UpvSGnMP0lLI
tx58dQjEl3lbis897KDgzHy2pGKQDcvLdj14/xpfjeTWHI6Ut3mZylIKWg==
=zWaw
-----END PGP PUBLIC KEY BLOCK-----
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Krystie Gate — static check stage of the CI gate.
Runs inside the Gitea Actions runner. Inspects all commits that were just
pushed to a krystie-wip/* branch and rejects if any violates the gate rules.
Decision per commit:
* If signed by Krystie's GPG key (fingerprint DCF2579968107984), apply the
full per-repo gate.
* If signed by a different key OR unsigned, allow (Sami's authority).
Per-repo enforcement:
* triangles_v5 : red-list (consensus paths) + test-first + no-clearnet
* triangles-explorer, triangles-api, tridock-web-wallet, sami-chat, tri-pi:
test-first only
* homebrew-triangles: formula syntax check only
Outputs:
* On reject, prints REJECTED lines to stderr and exits 1.
* On accept, sets `is_krystie_commit` GH-actions output to true/false.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
# Krystie's GPG identity. We accept both the primary long-ID and the
# signing subkey because `git log %GK` returns the subkey that was actually
# used to sign, not the primary. The full primary fingerprint is also
# included so a paranoid future check can validate the chain.
KRYSTIE_PRIMARY_FP = "523A81833EB7201573E1EFE1DCF2579968107984"
KRYSTIE_KEY_IDS = {
"DCF2579968107984", # primary long-ID
"C2DC60618C85A159", # signing subkey long-ID
}
RED_LIST_TRIANGLES_V5 = [
re.compile(r"^src/main\.(cpp|h)$"),
re.compile(r"^src/validation.*"),
re.compile(r"^src/kernel\.(cpp|h)$"),
re.compile(r"^src/checkpoints\.(cpp|h)$"),
re.compile(r"^src/consensus/"),
re.compile(r"^src/protocol\.(cpp|h)$"),
re.compile(r"^src/net\.(cpp|h)$"),
re.compile(r"^src/netbase\.(cpp|h)$"),
re.compile(r"^src/net_bootstrap\.(cpp|h)$"),
re.compile(r"^src/chainparams.*"),
re.compile(r"^src/clientversion\.h$"),
re.compile(r"^src/key\.(cpp|h)$"),
re.compile(r"^src/keystore\.(cpp|h)$"),
re.compile(r"^src/onionseed\.h$"),
re.compile(r"^contrib/seeds/"),
re.compile(r"^contrib/devtools/release.*"),
re.compile(r"^doc/release-process\.txt$"),
]
TEST_DIRS = {
"triangles_v5": ["src/test/", "test/"],
"triangles-explorer": ["src/__tests__/", "tests/", "test/"],
"triangles-api": ["test/", "__tests__/", "tests/"],
"tridock-web-wallet": ["test/", "__tests__/", "tests/"],
"sami-chat": ["test/", "__tests__/", "tests/"],
"tri-pi": ["test/", "tests/"],
"homebrew-triangles": [],
}
SOURCE_EXTS = {
"triangles_v5": {".cpp", ".h", ".c"},
"triangles-explorer": {".ts", ".tsx", ".js", ".svelte"},
"triangles-api": {".js", ".ts"},
"tridock-web-wallet": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"sami-chat": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"tri-pi": {".py", ".sh", ".ts", ".js"},
"homebrew-triangles": set(),
}
RED_LIST_REPOS = {"triangles_v5"}
PEER_CONFIG_PATHS = [
re.compile(r"^contrib/seeds/"),
re.compile(r"^src/chainparams.*"),
re.compile(r".*triangles\.conf(\.example)?$"),
]
@dataclass
class GateResult:
ok: bool
reason: str = ""
def repo_name() -> str:
repo = os.environ.get("GITHUB_REPOSITORY", "")
return repo.split("/", 1)[1] if "/" in repo else repo
def commit_signer(sha: str) -> str | None:
try:
out = subprocess.run(
["git", "log", "-1", "--format=%GK", sha],
check=True, capture_output=True, text=True,
).stdout.strip()
return out or None
except subprocess.CalledProcessError:
return None
def is_krystie_commit(sha: str) -> bool:
fp = commit_signer(sha)
if not fp:
return False
# Accept any key ID we know belongs to Krystie. `git log %GK` returns the
# signing subkey, so we have to whitelist both primary and subkey.
return any(fp == known or known.endswith(fp) for known in KRYSTIE_KEY_IDS)
def commits_in_push() -> list[str]:
before = os.environ.get("GITHUB_BEFORE", "")
sha = os.environ.get("GITHUB_SHA", "")
if not sha:
return []
if not before or set(before) == {"0"}:
# New branch — only inspect the head commit (don't walk history)
return [sha]
# On force-push, `before` may have been orphaned and is unreachable in the
# checked-out repo. `git rev-list before..sha` then exits 128. Fall back
# to inspecting the new head only — that's the safest guarantee we can
# make about what just landed.
try:
out = subprocess.run(
["git", "rev-list", f"{before}..{sha}"],
check=True, capture_output=True, text=True,
).stdout
return [c for c in out.split() if c]
except subprocess.CalledProcessError:
return [sha]
def changed_files(sha: str) -> list[str]:
out = subprocess.run(
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha],
check=True, capture_output=True, text=True,
).stdout
return [f for f in out.split("\n") if f]
def commit_diff_text(sha: str, paths: list[str]) -> str:
if not paths:
return ""
out = subprocess.run(
["git", "show", "--no-color", sha, "--"] + paths,
check=True, capture_output=True, text=True,
).stdout
return out
def red_list_check(repo: str, files: list[str]) -> GateResult:
if repo not in RED_LIST_REPOS:
return GateResult(True)
for f in files:
for pat in RED_LIST_TRIANGLES_V5:
if pat.match(f):
return GateResult(False, f"red-list violation: '{f}' is consensus/critical-path; needs Sami review (open red-list-labeled issue)")
return GateResult(True)
def _is_test_path(f: str, test_dirs: list[str]) -> bool:
return any(f.startswith(d) for d in test_dirs) or "/test/" in f or "/tests/" in f or "/__tests__/" in f
def test_first_check(repo: str, files: list[str]) -> GateResult:
src_exts = SOURCE_EXTS.get(repo, set())
test_dirs = TEST_DIRS.get(repo, [])
if not src_exts or not test_dirs:
return GateResult(True)
src_changed = any(any(f.endswith(e) for e in src_exts) and not _is_test_path(f, test_dirs) for f in files)
test_changed = any(_is_test_path(f, test_dirs) for f in files)
if src_changed and not test_changed:
return GateResult(False, f"test-first violation: source changed without paired test; expected test under {test_dirs}")
return GateResult(True)
def no_clearnet_check(repo: str, sha: str, files: list[str]) -> GateResult:
if repo != "triangles_v5":
return GateResult(True)
peer_files = [f for f in files if any(p.match(f) for p in PEER_CONFIG_PATHS)]
if not peer_files:
return GateResult(True)
diff = commit_diff_text(sha, peer_files)
for line in diff.split("\n"):
if not line.startswith("+") or line.startswith("+++"):
continue
body = line[1:].strip()
if re.search(r"\b(addnode|seednode|connect)\s*=", body, re.IGNORECASE):
if ".onion" not in body.lower():
return GateResult(False, f"no-clearnet: added peer/seed without .onion: {body[:120]}")
if re.match(r"^\s*(\d{1,3}\.){3}\d{1,3}\b", body) or re.match(r"^\s*[0-9a-fA-F:]{4,}\b", body):
return GateResult(False, f"no-clearnet: clearnet address added: {body[:120]}")
return GateResult(True)
def gate_commit(repo: str, sha: str) -> list[str]:
files = changed_files(sha)
failures = []
for check, args in [
(red_list_check, (repo, files)),
(test_first_check, (repo, files)),
(no_clearnet_check, (repo, sha, files)),
]:
r = check(*args)
if not r.ok:
failures.append(f"commit {sha[:12]}: {r.reason}")
return failures
def emit_output(name: str, value: str):
out_file = os.environ.get("GITHUB_OUTPUT", "")
if out_file:
with open(out_file, "a") as fh:
fh.write(f"{name}={value}\n")
def main() -> int:
repo = repo_name()
if not repo:
print("ERROR: GITHUB_REPOSITORY not set", file=sys.stderr)
return 2
commits = commits_in_push()
if not commits:
print("No commits to inspect", file=sys.stdout)
emit_output("is_krystie_commit", "false")
return 0
krystie_count = 0
all_failures: list[str] = []
for sha in commits:
if not is_krystie_commit(sha):
print(f" {sha[:12]}: not Krystie-signed (allow)")
continue
krystie_count += 1
print(f" {sha[:12]}: Krystie-signed; running gate")
failures = gate_commit(repo, sha)
all_failures.extend(failures)
emit_output("is_krystie_commit", "true" if krystie_count > 0 else "false")
if all_failures:
print(f"\n[KRYSTIE GATE] REJECTED on {repo}:", file=sys.stderr)
for f in all_failures:
print(f" - {f}", file=sys.stderr)
return 1
print(f"[KRYSTIE GATE] PASS on {repo} ({krystie_count} Krystie commit(s) inspected, {len(commits) - krystie_count} non-Krystie)")
return 0
if __name__ == "__main__":
sys.exit(main())
+132
View File
@@ -0,0 +1,132 @@
name: Krystie Gate
# Runs on every push to krystie-wip/* branches.
# Static checks first (cheap), then build + tests.
# If everything green AND the commit is Krystie's, fast-forwards master.
# Sami's pushes (admin) bypass this entire flow — he goes direct to master.
on:
push:
branches:
- 'krystie-wip/**'
jobs:
static-gate:
name: "Static gate (red-list / test-first / no-clearnet)"
runs-on: ubuntu-latest
outputs:
is_krystie_commit: ${{ steps.gate.outputs.is_krystie_commit }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Import Krystie public key (for verification)
run: |
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
if [ -f .gitea/krystie-release.pub.asc ]; then
gpg --import .gitea/krystie-release.pub.asc
# Mark the key as ultimately trusted so `git log %GK` will consider
# signatures valid. Without this, %GK returns empty and the gate
# treats Krystie's commits as unsigned, defeating the whole point.
FP=$(gpg --list-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')
echo "${FP}:6:" | gpg --import-ownertrust
echo "Imported and trusted Krystie public key: ${FP}"
# Configure git to call gpg for verification (it does by default,
# but explicit doesn't hurt) and not to require signed-by-default.
git config --global gpg.program gpg
else
echo "WARN: .gitea/krystie-release.pub.asc not found — gate will treat all commits as non-Krystie (i.e. allow)"
fi
- name: Run gate
id: gate
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
run: |
python3 .gitea/krystie_gate.py
build-and-test:
name: "Build + ctest"
needs: static-gate
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config \
libssl-dev libboost-all-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libsodium-dev \
libsecp256k1-dev || true
# Some packages may not be available; the C++20 / RocksDB modernization
# is in flight, so missing deps are tolerable for v1 of the gate.
- name: Configure (daemon-only, no Qt)
run: |
mkdir -p build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_TESTS=ON \
-DBUILD_ROCKSDB=OFF \
|| (echo "::warning::CMake configure failed — likely WIP modernization. Allowing build skip for v1." && exit 0)
- name: Build
run: |
if [ -f build/build.ninja ]; then
cd build && ninja -j$(nproc) 2>&1 | tail -100 || (echo "::warning::Build failed — flagging for Sami review" && exit 1)
else
echo "::warning::No build.ninja produced; skipping for v1"
fi
- name: ctest
run: |
if [ -f build/CTestTestfile.cmake ]; then
cd build && ctest --output-on-failure -j$(nproc) || exit 1
else
echo "::warning::No ctest produced; skipping for v1 — Krystie should add tests in src/test/"
fi
auto-merge:
name: "Auto-merge to master"
needs: [static-gate, build-and-test]
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' && needs.build-and-test.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
- name: Fast-forward master to this branch
env:
GITEA_TOKEN: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# The wip branch is master + N Krystie commits. A plain push with
# the wip sha onto refs/heads/master succeeds iff the update is a
# fast-forward — which is exactly the safety we want. (Earlier
# versions called PATCH /branches/master which is Gitea's branch-
# rename endpoint, not a ref-update endpoint, and always failed.)
REPO="${GITHUB_REPOSITORY}" # owner/name
GIT_URL="http://localhost:3030/${REPO}.git"
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" "${SHA}:refs/heads/master" \
&& echo "Master fast-forwarded to ${SHA:0:12}" \
|| (echo "::error::Fast-forward push refused — master has likely diverged" && exit 1)
# Clean up the wip branch via the same push channel (delete = empty source).
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" ":refs/heads/${BRANCH}" \
&& echo "Cleaned up wip branch ${BRANCH}" \
|| echo "::warning::Could not delete wip branch (it'll get pruned later)"
+11 -94
View File
@@ -2,7 +2,7 @@ name: Build All Platforms
on:
push:
branches: [master]
branches: [master, cpp20-modernization]
tags: ['v*']
pull_request:
branches: [master]
@@ -56,6 +56,8 @@ jobs:
SAN_FLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
@@ -268,6 +270,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON
@@ -275,16 +278,10 @@ jobs:
run: |
cmake --build build -j$(nproc)
strip --strip-all build/bin/trianglesd.exe
strip --strip-all build/bin/triangles-cli.exe
- name: Package daemon with DLLs
run: |
mkdir -p daemon-dist/tor
cp build/bin/trianglesd.exe daemon-dist/
# Copy all linked DLLs from MSYS2
ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' | while read dll; do
cp "$dll" daemon-dist/ 2>/dev/null || true
done
run: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli
- name: Bundle Tor for daemon
shell: powershell
@@ -454,6 +451,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON
@@ -461,93 +459,12 @@ jobs:
run: cmake --build build -j$(nproc)
- name: Strip binary
run: strip --strip-all build/bin/trianglesd
run: |
strip --strip-all build/bin/trianglesd
strip --strip-all build/bin/triangles-cli
- name: Build .deb package (fully self-contained)
run: |
TOR_VERSION="15.0.9"
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz
mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract
PKG="cryptographic-triangles-daemon_${VERSION}_amd64"
mkdir -p ${PKG}/DEBIAN
mkdir -p ${PKG}/usr/lib/cryptographic-triangles/lib
mkdir -p ${PKG}/usr/lib/cryptographic-triangles/tor
mkdir -p ${PKG}/usr/bin
mkdir -p ${PKG}/etc/systemd/system
cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/
cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/
chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/tor
[ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data
# Bundle ALL shared library dependencies (except glibc/kernel)
ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' | while read lib; do
case "$lib" in
/lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*)
;; # Skip glibc core — always present
*)
cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ 2>/dev/null || true
;;
esac
done
echo "=== Bundled libs ==="
ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l
ls ${PKG}/usr/lib/cryptographic-triangles/lib/
# Launcher with LD_LIBRARY_PATH
cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles
export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}"
exec "${INSTALL_DIR}/trianglesd" "$@"
LAUNCHER
sed -i 's/^ //' ${PKG}/usr/bin/trianglesd
chmod +x ${PKG}/usr/bin/trianglesd
cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC'
[Unit]
Description=Cryptographic Triangles Daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib
ExecStart=/usr/lib/cryptographic-triangles/trianglesd
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
SVC
sed -i 's/^ //' ${PKG}/etc/systemd/system/trianglesd.service
cat > ${PKG}/DEBIAN/control << CTRL
Package: cryptographic-triangles-daemon
Version: ${VERSION}
Architecture: amd64
Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org>
Description: Cryptographic Triangles daemon with integrated Tor
Fully self-contained headless node with all libraries, Tor, and systemd service.
No external dependencies required — runs on any x86_64 Linux.
Section: finance
Priority: optional
CTRL
sed -i 's/^ //' ${PKG}/DEBIAN/control
cat > ${PKG}/DEBIAN/postinst << 'POST'
#!/bin/bash
systemctl daemon-reload
echo ""
echo "Cryptographic Triangles daemon installed."
echo " Start: sudo systemctl start trianglesd"
echo " On boot: sudo systemctl enable trianglesd"
echo ""
POST
chmod +x ${PKG}/DEBIAN/postinst
dpkg-deb --build ${PKG}
run: bash scripts/ci/package-linux-daemon.sh "${VERSION}"
- name: Upload .deb
uses: actions/upload-artifact@v4
+1
View File
@@ -49,6 +49,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies + clang-tidy
run: |
+19 -2
View File
@@ -49,7 +49,6 @@ blocks/
# IDE
.vscode/
.idea/
.claude/
*.swp
*.swo
*~
@@ -68,6 +67,24 @@ triangles.conf
*.key
*.cert
*.gpg
*.o
src/trianglesd
src/obj/
build-bench/
build-cmake/
build-cmake-test/
build-latest/
build-rocks-probe/
build-rocksdb/
bench-results.csv
# Local build dirs (krystie)
/build-*/
/build/
/bench-results.csv
/build-rocks-probe/
/build-rocksdb/
/build-cmake/
/build-cmake-test/
/build-latest/
/build-bench/
/.qmake.stash
+3 -1
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif()
project(Triangles
VERSION 5.9.5
VERSION 6.0.0
DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX
)
@@ -47,6 +47,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# ── User-facing options ──
option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON)
option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON)
option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON)
option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON)
option(USE_UPNP "Enable UPnP support via miniupnpc" ON)
option(USE_IPV6 "Enable IPv6 support" ON)
@@ -185,6 +186,7 @@ message(STATUS "")
message(STATUS "Triangles ${PROJECT_VERSION} build configuration:")
message(STATUS " Build Qt GUI: ${BUILD_QT}")
message(STATUS " Build daemon: ${BUILD_DAEMON}")
message(STATUS " Build CLI: ${BUILD_CLI}")
message(STATUS " Build tests: ${BUILD_TESTS}")
message(STATUS " UPnP: ${USE_UPNP}")
message(STATUS " IPv6: ${USE_IPV6}")
+159
View File
@@ -0,0 +1,159 @@
# TRI v6 Development Task Queue
*Autonomous development pipeline — Krystie cycles through these continuously.*
## Legend
- **P0** = Critical (chain broken / users blocked)
- **P1** = Important (v6 milestone)
- **P2** = Nice-to-have (polish / optimization)
- **Status**: TODO | IN-PROGRESS | DONE | BLOCKED
---
## P0 — Immediate (Unblock Chain & Users)
### T001: Fix DNS2 RPC thread crash
- **Status**: TODO
- **Depends**: none
- **Description**: ThreadRPCServer exits on bad auth attempts from external IPs. Need to not kill the RPC thread on individual auth failures.
- **Files**: `src/rpc.cpp` or `src/bitcoinrpc.cpp`
- **Acceptance**: RPC stays up even with bad auth attempts; curl JSON-RPC works reliably
- **Model**: Claude Code or MiniMax M2.7
### T002: Fix DNS2 wallet 0 confirmed balance
- **Status**: TODO
- **Depends**: T001 (need reliable RPC)
- **Description**: Wallet restored from April 20 backup. Shows 11.24 TRI unconfirmed. Need to verify rescan completes and coins mature (520 confirmations) for staking.
- **Files**: wallet.dat, `src/wallet.cpp`
- **Acceptance**: Wallet shows confirmed balance after rescan + confirmations
- **Model**: Krystie (manual investigation, not subagent)
### T003: Fix seeds.txt parsing (only returns 1 address)
- **Status**: TODO
- **Depends**: none
- **Description**: HTTPS fetch of seeds.cryptographic-triangles.org/seeds.txt only returns 1 address. Possible comment parsing bug in net.cpp seed fetch logic.
- **Files**: `src/net.cpp`, `/var/www/seeds/seeds.txt`
- **Acceptance**: All 7 onion addresses returned on fetch
- **Model**: ZAI GLM-5.1
### T004: Fix Sami's PC wallet block 570 stall
- **Status**: IN-PROGRESS
- **Depends**: Windows binary build (DONE — built on sami-pc)
- **Description**: Windows Qt wallet stuck at block 570. GUI bootstrap fix committed (d0fb2dc). New binary built at E:\repos\triangles_v5\build-mingw\bin\triangles-qt.exe. Needs testing.
- **Acceptance**: Windows wallet syncs past block 570 with bootstrap
- **Model**: Krystie (manual deployment)
---
## P1 — v6 Core Milestones
### T010: Complete RocksDB runtime testing
- **Status**: TODO
- **Depends**: T001
- **Description**: RocksDB backend compiles clean but never tested with actual blockchain data. Need to: start daemon with `-rocksdb`, let it index chain, verify block lookups work, compare performance vs LevelDB.
- **Files**: `src/txdb.h`, `src/txdb.cpp`, `src/utxosnapshot.cpp`
- **Acceptance**: Daemon runs with `-rocksdb` flag, processes blocks, RPC queries return correct data
- **Model**: MiniMax M2.7
### T011: Wire UTXO snapshot P2P distribution (SnapshotNet)
- **Status**: TODO
- **Depends**: T010
- **Description**: `snapshotnet.cpp` exists but is placeholder. Need to implement: peer advertisement of snapshot availability, chunk transfer protocol, hash verification, integration with bootstrap flow.
- **Files**: `src/snapshotnet.cpp`, `src/net.cpp`, `src/utxosnapshot.cpp`
- **Acceptance**: New node can get UTXO snapshot from peers via P2P (not just HTTPS)
- **Model**: Claude Code + MiniMax M2.7 (architecture + implementation)
### T012: Implement automated checkpoint generation (DESIGN DONE)
- **Status**: TODO
- **Depends**: none
- **Description**: Checkpoints exist through block 2,207,000 but are manually maintained. Need automated checkpoint generation: every N blocks, compute checkpoint hash, push to code or external manifest.
- **Files**: `src/checkpoints.cpp`, `src/checkpoints.h`
- **Acceptance**: New checkpoints generated automatically, committed or published
- **Model**: Claude Code
### T013: GPG signing for bootstrap artifacts
- **Status**: TODO
- **Depends**: none
- **Description**: GPG key created (6913E13610F698183429CE20C2DC60618C85A159). Need to: sign every bootstrap/snapshot artifact on generation, verify signature on download, publish public key.
- **Files**: `/usr/local/bin/auto-update.sh`, `src/bootstrap.cpp`
- **Acceptance**: `gpg --verify` works on downloaded artifacts
- **Model**: ZAI GLM-5.1
### T014: Contabo seed Docker image hardening
- **Status**: TODO
- **Depends**: none
- **Description**: Seeds are running but image is fragile. Need: proper Dockerfile with version pinning, health checks, auto-restart, log shipping, and persistent volumes.
- **Files**: `/tmp/Dockerfile` on Contabo, `/tri/seed-{1..4}/`
- **Acceptance**: Seeds survive host reboot, auto-restart on crash, health check endpoint
- **Model**: ZAI GLM-5.1
### T015: Network health dashboard
- **Status**: TODO
- **Depends**: T001, T003
- **Description**: Operator-facing dashboard showing: block height per node, peer count, staking weight, chain sync status, seed health. Could be a simple web page served from DNS2.
- **Files**: New — `src/rpcblockchain.cpp` (health endpoint), frontend
- **Acceptance**: Live page showing all 7 nodes' status updated every 30s
- **Model**: MiniMax M2.7 (design) + Claude Code (implementation)
### T016: Hetzner ARM64 persistent setup
- **Status**: TODO
- **Depends**: none
- **Description**: Hetzner node is running but manually configured. Need: systemd service, auto-start on boot, bootstrap automation, monitoring.
- **Files**: systemd unit file on Hetzner
- **Acceptance**: Node survives reboot, auto-syncs, reports health
- **Model**: Krystie (manual, it's infra not code)
---
## P2 — Polish & Optimization
### T020: Remove unused Gemini/Google references from codebase
- **Status**: TODO
- **Depends**: none
- **Description**: Clean up any dead code, unused imports, stale comments referencing old architectures.
- **Model**: ZAI GLM-5.1
### T021: Comprehensive test suite
- **Status**: TODO
- **Depends**: T010
- **Description**: Expand test coverage for: UTXO snapshot load/dump, RocksDB backend, bootstrap download, seed fetch, checkpoint verification.
- **Files**: `src/test/`
- **Acceptance**: `test_triangles` passes with < 5 pre-existing failures
- **Model**: ZAI GLM-5.1 + MiniMax M2.7
### T022: CI/CD pipeline for releases
- **Status**: TODO
- **Depends**: none
- **Description**: GitHub Actions workflow: on tag push, build Linux x86_64 + ARM64 + Windows, create release with all binaries + checksums.
- **Files**: `.github/workflows/build-all.yml`
- **Acceptance**: Tag push produces release with 3 platform binaries
- **Model**: ZAI GLM-5.1
### T023: TRIdock + tri-wallet-web consolidation
- **Status**: TODO
- **Depends**: none
- **Description**: TRIdock and tri-wallet-web appear to be near-duplicates. Evaluate and either consolidate or clearly separate concerns.
- **Model**: MiniMax M2.7 (analysis)
---
## Completed
### ✅ Windows GUI bootstrap fix (d0fb2dc)
- Removed `#ifndef QT_GUI` guard so auto-bootstrap runs in GUI wallet
- Added `uiInterface.InitMessage()` for progress display
### ✅ Windows native build on sami-pc
- Built `triangles-qt.exe` (26MB) and `trianglesd.exe` via MSYS2/MinGW64
- All dependencies found natively
### ✅ RocksDB integration complete (ac9c6fb)
- CActiveTxDB wrapper, dual-backend support, compiles clean
### ✅ All nodes updated to v5.9.7.0
- DNS2, DNS3, Hetzner, Contabo seeds all running latest
### ✅ Bootstrap infrastructure live
- HTTPS at bootstrap.cryptographic-triangles.org
- Tor hidden service serving nginx on port 8085
- Seeds.txt with 7 onion nodes
+7
View File
@@ -0,0 +1,7 @@
# Krystie runner log
This file records autonomous-runner activity. Each entry is a doc-only
edit produced by the demo worker; once OpenClaw is wired in this log
will be replaced by real work.
- [2026-04-29T06:57:30Z] triangles_v5#1 — Smoke-test the Krystie loop runner
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# scripts/ci/package-linux-daemon.sh
#
# Linux packaging step for the triangles daemon + CLI .deb.
# Called from .github/workflows/build-all.yml build-linux-daemon step.
#
# Builds a self-contained .deb with trianglesd, triangles-cli, bundled libs,
# Tor, systemd service, and CLI launchers. Designed to be reproducible and
# debuggable outside the CI environment.
#
# Usage: bash scripts/ci/package-linux-daemon.sh <version>
set -euo pipefail
VERSION="${1:-0.0.0}"
PKG="cryptographic-triangles-daemon_${VERSION}_amd64"
TOR_VERSION="${TOR_VERSION:-15.0.9}"
echo ">>> Building .deb for triangles ${VERSION}"
# Stage directories
rm -rf "${PKG}"
mkdir -p "${PKG}/DEBIAN"
mkdir -p "${PKG}/usr/lib/cryptographic-triangles/lib"
mkdir -p "${PKG}/usr/lib/cryptographic-triangles/tor"
mkdir -p "${PKG}/usr/bin"
mkdir -p "${PKG}/etc/systemd/system"
# Download + extract Tor
TOR_TARBALL="tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz"
if [ ! -f "${TOR_TARBALL}" ]; then
echo ">>> Downloading Tor ${TOR_VERSION}..."
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" -o "${TOR_TARBALL}"
fi
mkdir -p tor-extract
tar -xzf "${TOR_TARBALL}" -C tor-extract
# Copy binaries
cp "build/bin/trianglesd" "${PKG}/usr/lib/cryptographic-triangles/"
cp "build/bin/triangles-cli" "${PKG}/usr/lib/cryptographic-triangles/"
# Copy Tor
cp "tor-extract/tor/tor" "${PKG}/usr/lib/cryptographic-triangles/tor/"
chmod +x "${PKG}/usr/lib/cryptographic-triangles/tor/tor"
if [ -d "tor-extract/data" ]; then
cp -r "tor-extract/data" "${PKG}/usr/lib/cryptographic-triangles/tor/data"
fi
# Bundle shared library dependencies (skip glibc/kernel — always present)
echo ">>> Bundling shared library dependencies..."
ALL_LIBS="$(mktemp)"
trap 'rm -f "${ALL_LIBS}"' EXIT
for bin in trianglesd triangles-cli; do
ldd "build/bin/${bin}" 2>/dev/null \
| grep '=> /' \
| awk '{print $3}' \
>> "${ALL_LIBS}" || true
done
if [ -s "${ALL_LIBS}" ]; then
sort -u "${ALL_LIBS}" | while IFS= read -r lib; do
if [ -z "${lib}" ]; then continue; fi
case "${lib}" in
/lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*)
;; # Skip glibc core
*)
cp -L "${lib}" "${PKG}/usr/lib/cryptographic-triangles/lib/" 2>/dev/null || true
;;
esac
done
fi
echo ">>> Bundled libs:"
ls -la "${PKG}/usr/lib/cryptographic-triangles/lib/" | tail -n +2 | wc -l
# Launchers (set LD_LIBRARY_PATH for bundled libs)
cat > "${PKG}/usr/bin/trianglesd" << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles
export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}"
exec "${INSTALL_DIR}/trianglesd" "$@"
LAUNCHER
chmod +x "${PKG}/usr/bin/trianglesd"
cat > "${PKG}/usr/bin/triangles-cli" << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles
export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}"
exec "${INSTALL_DIR}/triangles-cli" "$@"
LAUNCHER
chmod +x "${PKG}/usr/bin/triangles-cli"
# systemd unit
cat > "${PKG}/etc/systemd/system/trianglesd.service" << 'SVC'
[Unit]
Description=Cryptographic Triangles Daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib
ExecStart=/usr/lib/cryptographic-triangles/trianglesd
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
SVC
# DEBIAN/control
cat > "${PKG}/DEBIAN/control" << CTRL
Package: cryptographic-triangles-daemon
Version: ${VERSION}
Architecture: amd64
Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org>
Description: Cryptographic Triangles daemon + CLI with integrated Tor
Fully self-contained headless node + JSON-RPC client with all libraries,
Tor, and systemd service. No external dependencies required.
Section: finance
Priority: optional
CTRL
# DEBIAN/postinst
cat > "${PKG}/DEBIAN/postinst" << 'POST'
#!/bin/bash
systemctl daemon-reload
echo ""
echo "Cryptographic Triangles daemon + CLI installed."
echo " Start daemon: sudo systemctl start trianglesd"
echo " On boot: sudo systemctl enable trianglesd"
echo " Use CLI: triangles-cli getinfo"
echo ""
POST
chmod +x "${PKG}/DEBIAN/postinst"
# Build the .deb
dpkg-deb --build "${PKG}"
echo ">>> Built: ${PKG}.deb"
ls -la "${PKG}.deb"
exit 0
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# scripts/ci/package-windows-daemon.sh
#
# Windows MSYS2 packaging step for the triangles daemon + CLI.
# Called from .github/workflows/build-all.yml build-windows-daemon step.
#
# Why a script file instead of inline YAML:
# The GitHub Actions msys2 shell wrapper has shown inconsistent handling of
# multi-line inline run: blocks under `set -e -o pipefail` (silent exits with
# code 1). A committed script file bypasses the YAML → shell translation
# quirks and gives us a known-good artifact that we can also run locally in
# MSYS2 for debugging.
#
# Usage: bash scripts/ci/package-windows-daemon.sh <dist-dir> <bin> [<bin> ...]
# Example: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli
set -euo pipefail
DIST="${1:-daemon-dist}"
shift
BINS=("$@")
if [ "${#BINS[@]}" -eq 0 ]; then
echo "Usage: $0 <dist-dir> <bin> [<bin> ...]" >&2
echo " e.g. $0 daemon-dist trianglesd triangles-cli" >&2
exit 2
fi
echo ">>> Package step: bins=${BINS[*]} dist=${DIST}"
# Make the dist directory
mkdir -p "${DIST}/tor"
# Copy each binary to dist/
for bin in "${BINS[@]}"; do
src="build/bin/${bin}.exe"
if [ ! -f "${src}" ]; then
echo "ERROR: ${src} not found" >&2
exit 3
fi
cp "${src}" "${DIST}/"
echo " copied ${src} -> ${DIST}/"
done
# Copy linked DLLs (union of all binaries' dependencies, deduped)
echo ">>> Collecting DLLs from ldd output..."
ALL_DLLS="$(mktemp)"
trap 'rm -f "${ALL_DLLS}"' EXIT
for bin in "${BINS[@]}"; do
src="build/bin/${bin}.exe"
ldd "${src}" 2>/dev/null \
| grep '/mingw64' \
| awk '{print $3}' \
>> "${ALL_DLLS}" || true
done
if [ ! -s "${ALL_DLLS}" ]; then
echo "WARNING: no /mingw64 DLLs found in ldd output for ${BINS[*]}" >&2
else
echo ">>> Copying $(sort -u "${ALL_DLLS}" | wc -l) unique DLLs..."
sort -u "${ALL_DLLS}" | while IFS= read -r dll; do
if [ -n "${dll}" ] && [ -f "${dll}" ]; then
cp "${dll}" "${DIST}/" || echo "WARN: failed to copy ${dll}" >&2
fi
done
fi
echo ">>> Package complete: $(ls -1 "${DIST}" | wc -l) files in ${DIST}/"
ls -la "${DIST}/"
exit 0
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
set -euo pipefail
# Fresh-datadir IBD smoke test for TRI.
# Goal: detect the classic "starts from zero but stalls early / loops around 570"
# failure mode, and verify that sync keeps making forward progress.
#
# Example:
# bash scripts/ibd-smoke-test.sh \
# --bin ./build/src/trianglesd \
# --bootstrap-url http://100.104.4.5:8085/triangles-bootstrap.tar.gz \
# --addnode 74.208.167.19 --addnode 194.233.88.206
BIN="${BIN:-./build/src/trianglesd}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-1800}" # 30 minutes target window
POLL_SECONDS="${POLL_SECONDS:-15}"
STALL_WINDOW_SECONDS="${STALL_WINDOW_SECONDS:-180}"
BOOTSTRAP_URL="${BOOTSTRAP_URL:-}"
WORKDIR="${WORKDIR:-}"
RPC_PORT="${RPC_PORT:-19192}"
P2P_PORT="${P2P_PORT:-24193}"
MIN_EXPECTED_HEIGHT="${MIN_EXPECTED_HEIGHT:-5000}"
ALLOW_IBD="${ALLOW_IBD:-0}"
WHITELIST="${WHITELIST:-127.0.0.1}"
ADDNODES=()
usage() {
cat <<EOF
Usage: $0 [options]
Options:
--bin PATH trianglesd binary (default: $BIN)
--bootstrap-url URL optional bootstrap tar.gz URL to preload
--workdir PATH use an explicit temp workdir
--rpc-port N RPC port for test node (default: $RPC_PORT)
--p2p-port N P2P port for test node (default: $P2P_PORT)
--timeout N total test timeout seconds (default: $TIMEOUT_SECONDS)
--poll N poll interval seconds (default: $POLL_SECONDS)
--stall-window N no-progress failure window seconds (default: $STALL_WINDOW_SECONDS)
--min-height N minimum expected height/progress floor (default: $MIN_EXPECTED_HEIGHT)
--allow-ibd allow test to pass while still in IBD if progress is strong
--addnode HOST trusted peer to add (repeatable)
-h, --help show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--bin) BIN="$2"; shift 2 ;;
--bootstrap-url) BOOTSTRAP_URL="$2"; shift 2 ;;
--workdir) WORKDIR="$2"; shift 2 ;;
--rpc-port) RPC_PORT="$2"; shift 2 ;;
--p2p-port) P2P_PORT="$2"; shift 2 ;;
--timeout) TIMEOUT_SECONDS="$2"; shift 2 ;;
--poll) POLL_SECONDS="$2"; shift 2 ;;
--stall-window) STALL_WINDOW_SECONDS="$2"; shift 2 ;;
--min-height) MIN_EXPECTED_HEIGHT="$2"; shift 2 ;;
--allow-ibd) ALLOW_IBD=1; shift ;;
--addnode) ADDNODES+=("$2"); shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ ! -x "$BIN" ]]; then
echo "ERROR: trianglesd binary not executable: $BIN" >&2
exit 2
fi
if [[ -z "$WORKDIR" ]]; then
WORKDIR="$(mktemp -d /tmp/tri-ibd-smoke-XXXXXX)"
fi
DATADIR="$WORKDIR/datadir"
mkdir -p "$DATADIR"
RPCUSER="tri_test"
RPCPASSWORD="tri_test_$(date +%s)_$RANDOM"
CONF="$DATADIR/triangles.conf"
cat > "$CONF" <<EOF
server=1
daemon=1
staking=0
listen=1
discover=0
upnp=0
tor=0
irc=0
dnsseed=1
checkpoints=1
rpcuser=$RPCUSER
rpcpassword=$RPCPASSWORD
rpcport=$RPC_PORT
port=$P2P_PORT
maxconnections=32
whitelist=$WHITELIST
logtimestamps=1
EOF
for host in "${ADDNODES[@]}"; do
echo "addnode=$host" >> "$CONF"
done
cleanup() {
"$BIN" -datadir="$DATADIR" -conf="$CONF" stop >/dev/null 2>&1 || true
sleep 2 || true
pkill -f "$DATADIR" >/dev/null 2>&1 || true
}
trap cleanup EXIT
if [[ -n "$BOOTSTRAP_URL" ]]; then
echo "[ibd-test] downloading bootstrap: $BOOTSTRAP_URL"
curl -L --fail --max-time 1800 "$BOOTSTRAP_URL" -o "$WORKDIR/bootstrap.tar.gz"
tar xzf "$WORKDIR/bootstrap.tar.gz" -C "$DATADIR"
rm -f "$DATADIR/database/log."* "$DATADIR/txleveldb/LOCK" "$DATADIR/smsgDB/LOCK" 2>/dev/null || true
fi
echo "[ibd-test] starting node from datadir: $DATADIR"
"$BIN" -daemon -datadir="$DATADIR" -conf="$CONF" >/dev/null
sleep 6
rpc() {
local method="$1"
local params="${2:-[]}"
curl -sS --fail --user "$RPCUSER:$RPCPASSWORD" \
--data-binary "{\"jsonrpc\":\"1.0\",\"id\":\"ibd\",\"method\":\"$method\",\"params\":$params}" \
-H 'content-type: text/plain;' "http://127.0.0.1:$RPC_PORT/"
}
extract_json() {
python3 -c 'import json,sys; obj=json.load(sys.stdin); print(obj["result"])'
}
extract_field() {
local field="$1"
python3 -c 'import json,sys; obj=json.load(sys.stdin); val=obj["result"].get(sys.argv[1]); print(val if val is not None else "")' "$field"
}
start_ts=$(date +%s)
last_progress_ts=$start_ts
last_height=-1
samples=0
same_570_loops=0
best_height=0
while true; do
now=$(date +%s)
elapsed=$((now - start_ts))
if (( elapsed > TIMEOUT_SECONDS )); then
echo "FAIL: timeout after ${elapsed}s"
break
fi
if info_json="$(rpc getblockchaininfo 2>/dev/null)"; then
height=$(printf '%s' "$info_json" | extract_field blocks)
ibd=$(printf '%s' "$info_json" | extract_field initialblockdownload)
headers=$(printf '%s' "$info_json" | extract_field headers)
else
height=""
ibd=""
headers=""
fi
peers=0
if peer_json="$(rpc getconnectioncount 2>/dev/null)"; then
peers=$(printf '%s' "$peer_json" | extract_json)
fi
if [[ -n "$height" && "$height" != "$last_height" ]]; then
last_progress_ts=$now
last_height="$height"
if (( height > best_height )); then
best_height=$height
fi
fi
log_file="$DATADIR/debug.log"
if [[ -f "$log_file" ]]; then
loop_hits=$(tail -n 400 "$log_file" | grep -c 'start=571' || true)
if (( loop_hits >= 3 )); then
same_570_loops=$loop_hits
fi
fi
echo "[ibd-test] t=${elapsed}s height=${height:-?} headers=${headers:-?} ibd=${ibd:-?} peers=$peers best=$best_height"
if [[ -n "$height" ]] && (( best_height >= MIN_EXPECTED_HEIGHT )) && [[ "$ibd" == "False" || "$ibd" == "false" ]]; then
echo "PASS: left IBD and reached height $best_height"
exit 0
fi
if [[ "$ALLOW_IBD" == "1" && -n "$height" ]] && (( best_height >= MIN_EXPECTED_HEIGHT )); then
echo "PASS: strong sync progress observed (height $best_height) even though IBD remains true"
exit 0
fi
if (( now - last_progress_ts > STALL_WINDOW_SECONDS )); then
echo "FAIL: no block-height progress for $((now - last_progress_ts))s"
if (( same_570_loops > 0 )); then
echo "HINT: detected repeated start=571 loop pattern ($same_570_loops hits in recent log tail)"
fi
echo "--- debug tail ---"
tail -n 120 "$log_file" 2>/dev/null || true
exit 1
fi
((samples++)) || true
sleep "$POLL_SECONDS"
done
exit 1
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
# ============================================================================
# Triangles UTXO Snapshot Signer
# ============================================================================
# Generates a UTXO snapshot from the current node, signs its provenance
# message with the wallet's signing address, and writes the signed manifest.
#
# Usage:
# ./sign-snapshot.sh [snapshot-name]
#
# Default snapshot name: tri-utxo-snapshot-<timestamp>.utx
# Output (in this dir):
# <snapshot-name> - the UTXO snapshot binary
# <snapshot-name>.sig - base64 signature
# <snapshot-name>.msg - signed message (human-readable provenance)
# <snapshot-name>.manifest.json - signed manifest (drop into bootstrap dir)
# <snapshot-name>.pubkey - signing address
#
# Requirements:
# - trianglesd running with RPC enabled
# - wallet unlocked (or passphrase set in triangles.conf)
# - jq installed (apt: jq / brew: jq)
#
# Verification:
# ./sign-snapshot.sh verify <manifest.json> <snapshot-file>
# OR via RPC:
# verifymessage <addr> <sig> <msg>
# ============================================================================
set -euo pipefail
# ----- Config (override via env) -----
RPC_USER="${RPC_USER:-trianglesrpc}"
RPC_PASS="${RPC_PASS:-2KVK2FvLZBW9Hxv4a2Uj3dMRDAXdh4ei6S5tdZ3z2Mme}"
RPC_HOST="${RPC_HOST:-127.0.0.1}"
RPC_PORT="${RPC_PORT:-19112}"
SIGN_ACCOUNT="${SIGN_ACCOUNT:-}" # blank = use default account
NHEADERS="${NHEADERS:-2000}"
SNAP_DIR="${SNAP_DIR:-.}"
# ----- Helpers -----
rpc() {
local method="$1"; shift
local params="$1"; shift || true
curl -s --user "${RPC_USER}:${RPC_PASS}" \
-X POST -H 'Content-Type: application/json' \
--data "{\"jsonrpc\":\"1.0\",\"method\":\"${method}\",\"params\":${params}}" \
"http://${RPC_HOST}:${RPC_PORT}/"
}
rpc_field() {
local method="$1"; shift
local params="$1"; shift || true
local field="$1"; shift
rpc "$method" "$params" | jq -r ".result.${field} // empty"
}
sha256_file() { sha256sum "$1" | awk '{print $1}'; }
# ----- Verify mode -----
if [[ "${1:-}" == "verify" ]]; then
MANIFEST="${2:?usage: $0 verify <manifest.json> <snapshot-file>}"
SNAP="${3:?usage: $0 verify <manifest.json> <snapshot-file>}"
ADDR=$(jq -r '.signing_address' "$MANIFEST")
SIG=$(jq -r '.signature' "$MANIFEST")
MSG=$(jq -r '.message' "$MANIFEST")
EXPECTED_SHA=$(jq -r '.snapshot_sha256' "$MANIFEST")
echo "==> Verifying snapshot provenance..."
echo " Address: $ADDR"
echo " Message: $MSG"
ACTUAL_SHA=$(sha256_file "$SNAP")
if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
echo "FAIL: snapshot sha256 mismatch"
echo " expected: $EXPECTED_SHA"
echo " actual: $ACTUAL_SHA"
exit 1
fi
echo "OK: sha256 matches"
PARAMS=$(jq -nc --arg a "$ADDR" --arg s "$SIG" --arg m "$MSG" \
'[$a, $s, $m]')
RESULT=$(rpc verifymessage "$PARAMS" | jq -r '.result')
if [[ "$RESULT" == "true" ]]; then
echo "OK: signature valid — snapshot was signed by $ADDR"
exit 0
else
echo "FAIL: signature does not verify"
exit 1
fi
fi
# ----- Generate + sign -----
SNAP_NAME="${1:-tri-utxo-snapshot-$(date -u +%Y%m%dT%H%M%SZ).utx}"
SNAP_PATH="${SNAP_DIR}/${SNAP_NAME}"
echo "==> Step 1/5: querying chain state..."
HEIGHT=$(rpc_field getblockcount '[]' '' || echo "")
if [[ -z "$HEIGHT" ]]; then
rpc_field getblockcount '[]' '' # re-run for error visibility
echo "FAIL: RPC getblockcount failed"; exit 1
fi
HEIGHT=$(rpc getblockcount '[]' | jq -r '.result')
BLOCKHASH=$(rpc getbestblockhash '[]' | jq -r '.result')
echo " height: $HEIGHT"
echo " blockhash:$BLOCKHASH"
echo "==> Step 2/5: selecting signing address..."
if [[ -n "$SIGN_ACCOUNT" ]]; then
PARAMS=$(jq -nc --arg a "$SIGN_ACCOUNT" '[$a]')
else
PARAMS='[""]'
fi
ADDR=$(rpc getaccountaddress "$PARAMS" | jq -r '.result')
echo " signer: $ADDR"
echo "==> Step 3/5: dumping UTXO snapshot..."
PARAMS=$(jq -nc --arg f "$SNAP_PATH" --argjson n "$NHEADERS" '[$f, $n]')
DUMP_RESULT=$(rpc dumputxoset "$PARAMS")
echo "$DUMP_RESULT" | jq -r '.result // .error.message // .'
SIZE=$(echo "$DUMP_RESULT" | jq -r '.result.file_size // empty')
if [[ -z "$SIZE" ]]; then
echo "FAIL: dumputxoset failed"; exit 1
fi
echo " size: $SIZE bytes"
echo "==> Step 4/5: signing provenance message..."
SHA=$(sha256_file "$SNAP_PATH")
MSG="Triangles UTXO Snapshot $(date -u +%Y-%m-%d): height=$HEIGHT hash=$BLOCKHASH sha256=$SHA"
echo " message: $MSG"
PARAMS=$(jq -nc --arg a "$ADDR" --arg m "$MSG" '[$a, $m]')
SIG=$(rpc signmessage "$PARAMS" | jq -r '.result')
echo " sig: $SIG"
echo "==> Step 5/5: writing manifest + sidecars..."
MANIFEST_PATH="${SNAP_PATH}.manifest.json"
jq -n \
--arg name "$SNAP_NAME" \
--arg height "$HEIGHT" \
--arg hash "$BLOCKHASH" \
--arg sha "$SHA" \
--arg size "$SIZE" \
--arg msg "$MSG" \
--arg sig "$SIG" \
--arg addr "$ADDR" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg ver "$(rpc getnetworkinfo '[]' | jq -r '.result.version // "unknown"')" \
'{
schema: "triangles-utxo-snapshot-signed/v1",
name: $name,
generated_utc: $ts,
daemon_version: $ver,
chain_tip: { height: ($height | tonumber), blockhash: $hash },
snapshot_sha256: $sha,
snapshot_bytes: ($size | tonumber),
signing_address: $addr,
message: $msg,
signature: $sig
}' > "$MANIFEST_PATH"
# Sidecar files for easy reading
echo "$ADDR" > "${SNAP_PATH}.pubkey"
echo "$MSG" > "${SNAP_PATH}.msg"
echo "$SIG" > "${SNAP_PATH}.sig"
echo ""
echo "============================================================"
echo "Snapshot signed."
echo " snapshot: $SNAP_PATH"
echo " signature: ${SNAP_PATH}.sig"
echo " manifest: $MANIFEST_PATH"
echo " signer: $ADDR"
echo " sha256: $SHA"
echo "============================================================"
echo ""
echo "To verify on any node:"
echo " verifymessage $ADDR \\"
echo " '$SIG' \\"
echo " '$MSG'"
echo ""
echo "Or run: $0 verify $MANIFEST_PATH $SNAP_PATH"
+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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+37
View File
@@ -42,6 +42,7 @@ set(CORE_SOURCES
bootstrap.cpp
checkpoints.cpp
crypter.cpp
hdwallet.cpp
crypto_ecdh.cpp
crypto_ecdsa.cpp
db.cpp
@@ -62,6 +63,8 @@ set(CORE_SOURCES
pbkdf2.cpp
scrypt.cpp
smessage.cpp
syncmanager.cpp
chaindb_migrate.cpp
tor_embed_hooks.cpp
rest.cpp
trianglesrpc.cpp
@@ -247,6 +250,39 @@ if(BUILD_DAEMON)
endif()
endif()
# ═══════════════════════════════════════════════════════════════════════════════
# 4b. JSON-RPC client (triangles-cli)
#
# Self-contained: only links univalue + boost::asio + boost::program_options
# + boost::filesystem + OpenSSL (for base64 / future TLS). Does NOT link
# triangles_common, wallet, or net — keeps the binary small.
# ═══════════════════════════════════════════════════════════════════════════════
if(BUILD_CLI)
add_executable(triangles-cli
triangles-cli.cpp
)
# No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links
# the json_compat header-only shim and the platform's native socket lib
# (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and
# avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names;
# Homebrew doesn't ship the boost_system CMake config).
target_link_libraries(triangles-cli
PRIVATE
json_compat
)
if(WIN32)
set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe")
target_link_libraries(triangles-cli PRIVATE ws2_32)
endif()
if(MSVC)
set_target_properties(triangles-cli PROPERTIES
VS_WINRT_COMPONENT "console"
)
endif()
endif()
# ═══════════════════════════════════════════════════════════════════════════════
# 5. Qt5 GUI wallet (triangles-qt)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -304,6 +340,7 @@ if(BUILD_QT)
qt/trianglesunits.cpp
qt/qvaluecombobox.cpp
qt/askpassphrasedialog.cpp
qt/hdseeddialog.cpp
qt/notificator.cpp
qt/qtipcserver.cpp
qt/rpcconsole.cpp
+263
View File
@@ -0,0 +1,263 @@
// BIP39 English wordlist (2048 words, canonical). Auto-generated; do not edit.
#ifndef TRIANGLES_BIP39_ENGLISH_H
#define TRIANGLES_BIP39_ENGLISH_H
static const char* const BIP39_WORDLIST_EN[2048] = {
"abandon","ability","able","about","above","absent","absorb","abstract",
"absurd","abuse","access","accident","account","accuse","achieve","acid",
"acoustic","acquire","across","act","action","actor","actress","actual",
"adapt","add","addict","address","adjust","admit","adult","advance",
"advice","aerobic","affair","afford","afraid","again","age","agent",
"agree","ahead","aim","air","airport","aisle","alarm","album",
"alcohol","alert","alien","all","alley","allow","almost","alone",
"alpha","already","also","alter","always","amateur","amazing","among",
"amount","amused","analyst","anchor","ancient","anger","angle","angry",
"animal","ankle","announce","annual","another","answer","antenna","antique",
"anxiety","any","apart","apology","appear","apple","approve","april",
"arch","arctic","area","arena","argue","arm","armed","armor",
"army","around","arrange","arrest","arrive","arrow","art","artefact",
"artist","artwork","ask","aspect","assault","asset","assist","assume",
"asthma","athlete","atom","attack","attend","attitude","attract","auction",
"audit","august","aunt","author","auto","autumn","average","avocado",
"avoid","awake","aware","away","awesome","awful","awkward","axis",
"baby","bachelor","bacon","badge","bag","balance","balcony","ball",
"bamboo","banana","banner","bar","barely","bargain","barrel","base",
"basic","basket","battle","beach","bean","beauty","because","become",
"beef","before","begin","behave","behind","believe","below","belt",
"bench","benefit","best","betray","better","between","beyond","bicycle",
"bid","bike","bind","biology","bird","birth","bitter","black",
"blade","blame","blanket","blast","bleak","bless","blind","blood",
"blossom","blouse","blue","blur","blush","board","boat","body",
"boil","bomb","bone","bonus","book","boost","border","boring",
"borrow","boss","bottom","bounce","box","boy","bracket","brain",
"brand","brass","brave","bread","breeze","brick","bridge","brief",
"bright","bring","brisk","broccoli","broken","bronze","broom","brother",
"brown","brush","bubble","buddy","budget","buffalo","build","bulb",
"bulk","bullet","bundle","bunker","burden","burger","burst","bus",
"business","busy","butter","buyer","buzz","cabbage","cabin","cable",
"cactus","cage","cake","call","calm","camera","camp","can",
"canal","cancel","candy","cannon","canoe","canvas","canyon","capable",
"capital","captain","car","carbon","card","cargo","carpet","carry",
"cart","case","cash","casino","castle","casual","cat","catalog",
"catch","category","cattle","caught","cause","caution","cave","ceiling",
"celery","cement","census","century","cereal","certain","chair","chalk",
"champion","change","chaos","chapter","charge","chase","chat","cheap",
"check","cheese","chef","cherry","chest","chicken","chief","child",
"chimney","choice","choose","chronic","chuckle","chunk","churn","cigar",
"cinnamon","circle","citizen","city","civil","claim","clap","clarify",
"claw","clay","clean","clerk","clever","click","client","cliff",
"climb","clinic","clip","clock","clog","close","cloth","cloud",
"clown","club","clump","cluster","clutch","coach","coast","coconut",
"code","coffee","coil","coin","collect","color","column","combine",
"come","comfort","comic","common","company","concert","conduct","confirm",
"congress","connect","consider","control","convince","cook","cool","copper",
"copy","coral","core","corn","correct","cost","cotton","couch",
"country","couple","course","cousin","cover","coyote","crack","cradle",
"craft","cram","crane","crash","crater","crawl","crazy","cream",
"credit","creek","crew","cricket","crime","crisp","critic","crop",
"cross","crouch","crowd","crucial","cruel","cruise","crumble","crunch",
"crush","cry","crystal","cube","culture","cup","cupboard","curious",
"current","curtain","curve","cushion","custom","cute","cycle","dad",
"damage","damp","dance","danger","daring","dash","daughter","dawn",
"day","deal","debate","debris","decade","december","decide","decline",
"decorate","decrease","deer","defense","define","defy","degree","delay",
"deliver","demand","demise","denial","dentist","deny","depart","depend",
"deposit","depth","deputy","derive","describe","desert","design","desk",
"despair","destroy","detail","detect","develop","device","devote","diagram",
"dial","diamond","diary","dice","diesel","diet","differ","digital",
"dignity","dilemma","dinner","dinosaur","direct","dirt","disagree","discover",
"disease","dish","dismiss","disorder","display","distance","divert","divide",
"divorce","dizzy","doctor","document","dog","doll","dolphin","domain",
"donate","donkey","donor","door","dose","double","dove","draft",
"dragon","drama","drastic","draw","dream","dress","drift","drill",
"drink","drip","drive","drop","drum","dry","duck","dumb",
"dune","during","dust","dutch","duty","dwarf","dynamic","eager",
"eagle","early","earn","earth","easily","east","easy","echo",
"ecology","economy","edge","edit","educate","effort","egg","eight",
"either","elbow","elder","electric","elegant","element","elephant","elevator",
"elite","else","embark","embody","embrace","emerge","emotion","employ",
"empower","empty","enable","enact","end","endless","endorse","enemy",
"energy","enforce","engage","engine","enhance","enjoy","enlist","enough",
"enrich","enroll","ensure","enter","entire","entry","envelope","episode",
"equal","equip","era","erase","erode","erosion","error","erupt",
"escape","essay","essence","estate","eternal","ethics","evidence","evil",
"evoke","evolve","exact","example","excess","exchange","excite","exclude",
"excuse","execute","exercise","exhaust","exhibit","exile","exist","exit",
"exotic","expand","expect","expire","explain","expose","express","extend",
"extra","eye","eyebrow","fabric","face","faculty","fade","faint",
"faith","fall","false","fame","family","famous","fan","fancy",
"fantasy","farm","fashion","fat","fatal","father","fatigue","fault",
"favorite","feature","february","federal","fee","feed","feel","female",
"fence","festival","fetch","fever","few","fiber","fiction","field",
"figure","file","film","filter","final","find","fine","finger",
"finish","fire","firm","first","fiscal","fish","fit","fitness",
"fix","flag","flame","flash","flat","flavor","flee","flight",
"flip","float","flock","floor","flower","fluid","flush","fly",
"foam","focus","fog","foil","fold","follow","food","foot",
"force","forest","forget","fork","fortune","forum","forward","fossil",
"foster","found","fox","fragile","frame","frequent","fresh","friend",
"fringe","frog","front","frost","frown","frozen","fruit","fuel",
"fun","funny","furnace","fury","future","gadget","gain","galaxy",
"gallery","game","gap","garage","garbage","garden","garlic","garment",
"gas","gasp","gate","gather","gauge","gaze","general","genius",
"genre","gentle","genuine","gesture","ghost","giant","gift","giggle",
"ginger","giraffe","girl","give","glad","glance","glare","glass",
"glide","glimpse","globe","gloom","glory","glove","glow","glue",
"goat","goddess","gold","good","goose","gorilla","gospel","gossip",
"govern","gown","grab","grace","grain","grant","grape","grass",
"gravity","great","green","grid","grief","grit","grocery","group",
"grow","grunt","guard","guess","guide","guilt","guitar","gun",
"gym","habit","hair","half","hammer","hamster","hand","happy",
"harbor","hard","harsh","harvest","hat","have","hawk","hazard",
"head","health","heart","heavy","hedgehog","height","hello","helmet",
"help","hen","hero","hidden","high","hill","hint","hip",
"hire","history","hobby","hockey","hold","hole","holiday","hollow",
"home","honey","hood","hope","horn","horror","horse","hospital",
"host","hotel","hour","hover","hub","huge","human","humble",
"humor","hundred","hungry","hunt","hurdle","hurry","hurt","husband",
"hybrid","ice","icon","idea","identify","idle","ignore","ill",
"illegal","illness","image","imitate","immense","immune","impact","impose",
"improve","impulse","inch","include","income","increase","index","indicate",
"indoor","industry","infant","inflict","inform","inhale","inherit","initial",
"inject","injury","inmate","inner","innocent","input","inquiry","insane",
"insect","inside","inspire","install","intact","interest","into","invest",
"invite","involve","iron","island","isolate","issue","item","ivory",
"jacket","jaguar","jar","jazz","jealous","jeans","jelly","jewel",
"job","join","joke","journey","joy","judge","juice","jump",
"jungle","junior","junk","just","kangaroo","keen","keep","ketchup",
"key","kick","kid","kidney","kind","kingdom","kiss","kit",
"kitchen","kite","kitten","kiwi","knee","knife","knock","know",
"lab","label","labor","ladder","lady","lake","lamp","language",
"laptop","large","later","latin","laugh","laundry","lava","law",
"lawn","lawsuit","layer","lazy","leader","leaf","learn","leave",
"lecture","left","leg","legal","legend","leisure","lemon","lend",
"length","lens","leopard","lesson","letter","level","liar","liberty",
"library","license","life","lift","light","like","limb","limit",
"link","lion","liquid","list","little","live","lizard","load",
"loan","lobster","local","lock","logic","lonely","long","loop",
"lottery","loud","lounge","love","loyal","lucky","luggage","lumber",
"lunar","lunch","luxury","lyrics","machine","mad","magic","magnet",
"maid","mail","main","major","make","mammal","man","manage",
"mandate","mango","mansion","manual","maple","marble","march","margin",
"marine","market","marriage","mask","mass","master","match","material",
"math","matrix","matter","maximum","maze","meadow","mean","measure",
"meat","mechanic","medal","media","melody","melt","member","memory",
"mention","menu","mercy","merge","merit","merry","mesh","message",
"metal","method","middle","midnight","milk","million","mimic","mind",
"minimum","minor","minute","miracle","mirror","misery","miss","mistake",
"mix","mixed","mixture","mobile","model","modify","mom","moment",
"monitor","monkey","monster","month","moon","moral","more","morning",
"mosquito","mother","motion","motor","mountain","mouse","move","movie",
"much","muffin","mule","multiply","muscle","museum","mushroom","music",
"must","mutual","myself","mystery","myth","naive","name","napkin",
"narrow","nasty","nation","nature","near","neck","need","negative",
"neglect","neither","nephew","nerve","nest","net","network","neutral",
"never","news","next","nice","night","noble","noise","nominee",
"noodle","normal","north","nose","notable","note","nothing","notice",
"novel","now","nuclear","number","nurse","nut","oak","obey",
"object","oblige","obscure","observe","obtain","obvious","occur","ocean",
"october","odor","off","offer","office","often","oil","okay",
"old","olive","olympic","omit","once","one","onion","online",
"only","open","opera","opinion","oppose","option","orange","orbit",
"orchard","order","ordinary","organ","orient","original","orphan","ostrich",
"other","outdoor","outer","output","outside","oval","oven","over",
"own","owner","oxygen","oyster","ozone","pact","paddle","page",
"pair","palace","palm","panda","panel","panic","panther","paper",
"parade","parent","park","parrot","party","pass","patch","path",
"patient","patrol","pattern","pause","pave","payment","peace","peanut",
"pear","peasant","pelican","pen","penalty","pencil","people","pepper",
"perfect","permit","person","pet","phone","photo","phrase","physical",
"piano","picnic","picture","piece","pig","pigeon","pill","pilot",
"pink","pioneer","pipe","pistol","pitch","pizza","place","planet",
"plastic","plate","play","please","pledge","pluck","plug","plunge",
"poem","poet","point","polar","pole","police","pond","pony",
"pool","popular","portion","position","possible","post","potato","pottery",
"poverty","powder","power","practice","praise","predict","prefer","prepare",
"present","pretty","prevent","price","pride","primary","print","priority",
"prison","private","prize","problem","process","produce","profit","program",
"project","promote","proof","property","prosper","protect","proud","provide",
"public","pudding","pull","pulp","pulse","pumpkin","punch","pupil",
"puppy","purchase","purity","purpose","purse","push","put","puzzle",
"pyramid","quality","quantum","quarter","question","quick","quit","quiz",
"quote","rabbit","raccoon","race","rack","radar","radio","rail",
"rain","raise","rally","ramp","ranch","random","range","rapid",
"rare","rate","rather","raven","raw","razor","ready","real",
"reason","rebel","rebuild","recall","receive","recipe","record","recycle",
"reduce","reflect","reform","refuse","region","regret","regular","reject",
"relax","release","relief","rely","remain","remember","remind","remove",
"render","renew","rent","reopen","repair","repeat","replace","report",
"require","rescue","resemble","resist","resource","response","result","retire",
"retreat","return","reunion","reveal","review","reward","rhythm","rib",
"ribbon","rice","rich","ride","ridge","rifle","right","rigid",
"ring","riot","ripple","risk","ritual","rival","river","road",
"roast","robot","robust","rocket","romance","roof","rookie","room",
"rose","rotate","rough","round","route","royal","rubber","rude",
"rug","rule","run","runway","rural","sad","saddle","sadness",
"safe","sail","salad","salmon","salon","salt","salute","same",
"sample","sand","satisfy","satoshi","sauce","sausage","save","say",
"scale","scan","scare","scatter","scene","scheme","school","science",
"scissors","scorpion","scout","scrap","screen","script","scrub","sea",
"search","season","seat","second","secret","section","security","seed",
"seek","segment","select","sell","seminar","senior","sense","sentence",
"series","service","session","settle","setup","seven","shadow","shaft",
"shallow","share","shed","shell","sheriff","shield","shift","shine",
"ship","shiver","shock","shoe","shoot","shop","short","shoulder",
"shove","shrimp","shrug","shuffle","shy","sibling","sick","side",
"siege","sight","sign","silent","silk","silly","silver","similar",
"simple","since","sing","siren","sister","situate","six","size",
"skate","sketch","ski","skill","skin","skirt","skull","slab",
"slam","sleep","slender","slice","slide","slight","slim","slogan",
"slot","slow","slush","small","smart","smile","smoke","smooth",
"snack","snake","snap","sniff","snow","soap","soccer","social",
"sock","soda","soft","solar","soldier","solid","solution","solve",
"someone","song","soon","sorry","sort","soul","sound","soup",
"source","south","space","spare","spatial","spawn","speak","special",
"speed","spell","spend","sphere","spice","spider","spike","spin",
"spirit","split","spoil","sponsor","spoon","sport","spot","spray",
"spread","spring","spy","square","squeeze","squirrel","stable","stadium",
"staff","stage","stairs","stamp","stand","start","state","stay",
"steak","steel","stem","step","stereo","stick","still","sting",
"stock","stomach","stone","stool","story","stove","strategy","street",
"strike","strong","struggle","student","stuff","stumble","style","subject",
"submit","subway","success","such","sudden","suffer","sugar","suggest",
"suit","summer","sun","sunny","sunset","super","supply","supreme",
"sure","surface","surge","surprise","surround","survey","suspect","sustain",
"swallow","swamp","swap","swarm","swear","sweet","swift","swim",
"swing","switch","sword","symbol","symptom","syrup","system","table",
"tackle","tag","tail","talent","talk","tank","tape","target",
"task","taste","tattoo","taxi","teach","team","tell","ten",
"tenant","tennis","tent","term","test","text","thank","that",
"theme","then","theory","there","they","thing","this","thought",
"three","thrive","throw","thumb","thunder","ticket","tide","tiger",
"tilt","timber","time","tiny","tip","tired","tissue","title",
"toast","tobacco","today","toddler","toe","together","toilet","token",
"tomato","tomorrow","tone","tongue","tonight","tool","tooth","top",
"topic","topple","torch","tornado","tortoise","toss","total","tourist",
"toward","tower","town","toy","track","trade","traffic","tragic",
"train","transfer","trap","trash","travel","tray","treat","tree",
"trend","trial","tribe","trick","trigger","trim","trip","trophy",
"trouble","truck","true","truly","trumpet","trust","truth","try",
"tube","tuition","tumble","tuna","tunnel","turkey","turn","turtle",
"twelve","twenty","twice","twin","twist","two","type","typical",
"ugly","umbrella","unable","unaware","uncle","uncover","under","undo",
"unfair","unfold","unhappy","uniform","unique","unit","universe","unknown",
"unlock","until","unusual","unveil","update","upgrade","uphold","upon",
"upper","upset","urban","urge","usage","use","used","useful",
"useless","usual","utility","vacant","vacuum","vague","valid","valley",
"valve","van","vanish","vapor","various","vast","vault","vehicle",
"velvet","vendor","venture","venue","verb","verify","version","very",
"vessel","veteran","viable","vibrant","vicious","victory","video","view",
"village","vintage","violin","virtual","virus","visa","visit","visual",
"vital","vivid","vocal","voice","void","volcano","volume","vote",
"voyage","wage","wagon","wait","walk","wall","walnut","want",
"warfare","warm","warrior","wash","wasp","waste","water","wave",
"way","wealth","weapon","wear","weasel","weather","web","wedding",
"weekend","weird","welcome","west","wet","whale","what","wheat",
"wheel","when","where","whip","whisper","wide","width","wife",
"wild","will","win","window","wine","wing","wink","winner",
"winter","wire","wisdom","wise","wish","witness","wolf","woman",
"wonder","wood","wool","word","work","world","worry","worth",
"wrap","wreck","wrestle","wrist","write","wrong","yard","year",
"yellow","you","young","youth","zebra","zero","zone","zoo",
};
#endif
+338 -9
View File
@@ -17,6 +17,13 @@
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/sha.h>
#include "key.h"
#include "base58.h"
#include "util.h"
extern const std::string strMessageMagic;
#include <fstream>
#include <sstream>
@@ -43,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.
@@ -720,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";
@@ -753,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))
@@ -771,15 +785,312 @@ bool DownloadBootstrap(const std::string& host,
return true;
}
namespace {
// Try to find the canonical UTXO snapshot entry in the bootstrap server's
// manifest.json. Looks for an entry of type "utxo_snapshot" and extracts
// its filename + expected SHA256. Returns true on success.
//
// We deliberately do a simple substring scan rather than full JSON parsing:
// the manifest is operator-controlled, the format is stable, and adding a
// JSON dependency for ~50 lines of code isn't worth it.
//
// On failure, the caller falls back to the legacy "utxo-snapshot.bin" URL,
// which the bootstrap server symlinks to the canonical file.
// Trusted signer addresses for snapshot manifests. A snapshot is accepted
// iff its manifest's signing_address matches one of these AND its signature
// verifies under Triangles' compact-message protocol.
static const char* TRUSTED_SNAPSHOT_SIGNERS[] = {
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's snapshot publisher key
};
static const size_t NUM_TRUSTED_SNAPSHOT_SIGNERS =
sizeof(TRUSTED_SNAPSHOT_SIGNERS) / sizeof(TRUSTED_SNAPSHOT_SIGNERS[0]);
bool IsTrustedSnapshotSigner(const std::string& addr)
{
for (size_t i = 0; i < NUM_TRUSTED_SNAPSHOT_SIGNERS; ++i)
if (addr == TRUSTED_SNAPSHOT_SIGNERS[i])
return true;
return false;
}
// Verify a Triangles signed-message compact signature. Returns true iff:
// - The address is valid
// - The signature is valid base64
// - The compact signature recovers to a public key whose hash160 matches
// the address's keyID
// - The hash being verified is Hash(strMessageMagic || message)
//
// Mirrors verifymessage RPC. Caller separately checks trust.
bool VerifySignedMessage(const std::string& strAddress,
const std::string& strSignatureB64,
const std::string& strMessage,
std::string& strError)
{
CTrianglesAddress addr(strAddress);
if (!addr.IsValid()) {
strError = "Invalid signer address: " + strAddress;
return false;
}
CKeyID keyID;
if (!addr.GetKeyID(keyID)) {
strError = "Address does not refer to a key: " + strAddress;
return false;
}
bool fInvalid = false;
std::vector<unsigned char> vchSig = DecodeBase64(strSignatureB64.c_str(), &fInvalid);
if (fInvalid) {
strError = "Malformed base64 in signature";
return false;
}
CDataStream ss(SER_GETHASH, 0);
ss << strMessageMagic;
ss << strMessage;
CKey key;
if (!key.SetCompactSignature(Hash(ss.begin(), ss.end()), vchSig)) {
strError = "Signature does not verify (recovered key mismatch or malformed sig)";
return false;
}
if (key.GetPubKey().GetID() != keyID) {
strError = "Signature recovered to a different key than the claimed signer";
return false;
}
return true;
}
// Extract a string field value from a small JSON object (subset).
std::string ExtractJsonString(const std::string& json, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = json.find(key);
if (pos == std::string::npos) return "";
pos += key.size();
while (pos < json.size() && (json[pos] == ' ' || json[pos] == ':' || json[pos] == '\t'))
pos++;
if (pos >= json.size() || json[pos] != '\"') return "";
pos++;
size_t end = json.find('\"', pos);
if (end == std::string::npos) return "";
return json.substr(pos, end - pos);
}
bool FindCanonicalSnapshotInManifest(const std::string& manifestText,
std::string& outFilename,
std::string& outSha256,
std::string& outManifestFilename,
std::string& strError)
{
// Look for the "utxo_snapshot" file entry, e.g.:
// "utxo-snapshot-2207680.utx": {
// ...
// "type": "utxo_snapshot",
// "sha256": "eeefe107...",
// ...
// }
size_t typePos = manifestText.find("\"utxo_snapshot\"");
if (typePos == std::string::npos) {
strError = "manifest.json has no utxo_snapshot entry";
return false;
}
// Walk backwards from the typePos to find the start of this file's block.
// Format: "filename": { ... "type": "utxo_snapshot" ...
// We scan for the nearest preceding '"' followed by ':' that introduces a
// top-level file entry. Simple heuristic: find the line containing the
// type marker, then search backwards for the file key.
size_t entryStart = manifestText.rfind('"', typePos);
if (entryStart == std::string::npos || entryStart == 0) {
strError = "malformed manifest.json (no filename before utxo_snapshot entry)";
return false;
}
// Skip the opening quote
size_t filenameStart = entryStart + 1;
size_t filenameEnd = manifestText.find('"', filenameStart);
if (filenameEnd == std::string::npos) {
strError = "malformed manifest.json (unterminated filename)";
return false;
}
outFilename = manifestText.substr(filenameStart, filenameEnd - filenameStart);
// Within this block, extract the sha256.
// Walk forward from the typePos to find the matching closing brace of the
// entry. (Manifest is shallow, so a naive brace-count is fine.)
size_t braceStart = manifestText.find('{', filenameEnd);
if (braceStart == std::string::npos) {
strError = "malformed manifest.json (no body after filename)";
return false;
}
int depth = 0;
size_t bodyEnd = braceStart;
for (size_t i = braceStart; i < manifestText.size(); ++i) {
if (manifestText[i] == '{') depth++;
else if (manifestText[i] == '}') {
depth--;
if (depth == 0) { bodyEnd = i; break; }
}
}
if (depth != 0) {
strError = "malformed manifest.json (unbalanced braces in entry)";
return false;
}
std::string entry = manifestText.substr(braceStart, bodyEnd - braceStart);
size_t shaPos = entry.find("\"sha256\"");
if (shaPos == std::string::npos) {
strError = "manifest entry has no sha256 field";
return false;
}
size_t valStart = entry.find('"', shaPos + 8);
if (valStart == std::string::npos) {
strError = "malformed manifest.json (no sha256 value)";
return false;
}
valStart++;
size_t valEnd = entry.find('"', valStart);
if (valEnd == std::string::npos) {
strError = "malformed manifest.json (unterminated sha256 value)";
return false;
}
outSha256 = entry.substr(valStart, valEnd - valStart);
// Extract manifest filename (optional).
outManifestFilename.clear();
size_t manPos = entry.find("\"manifest\"");
if (manPos != std::string::npos) {
size_t mvStart = entry.find('\"', manPos + 10);
if (mvStart != std::string::npos) {
mvStart++;
size_t mvEnd = entry.find('\"', mvStart);
if (mvEnd != std::string::npos)
outManifestFilename = entry.substr(mvStart, mvEnd - mvStart);
}
}
return true;
}
// Read an entire file into a string. Empty string on error.
std::string ReadFileToString(const fs::path& path)
{
FILE* f = fopen(path.string().c_str(), "rb");
if (!f) return "";
fseek(f, 0, SEEK_END);
long sz = ftell(f);
if (sz < 0) { fclose(f); return ""; }
fseek(f, 0, SEEK_SET);
std::string s(sz, '\0');
size_t nread = fread(&s[0], 1, sz, f);
s.resize(nread);
fclose(f);
return s;
}
// Compute the SHA256 of a file, return as lowercase hex string.
std::string Sha256OfFile(const fs::path& path)
{
FILE* f = fopen(path.string().c_str(), "rb");
if (!f) return "";
SHA256_CTX ctx;
SHA256_Init(&ctx);
unsigned char buf[64 * 1024];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
SHA256_Update(&ctx, buf, n);
fclose(f);
unsigned char out[SHA256_DIGEST_LENGTH];
SHA256_Final(out, &ctx);
static const char hex[] = "0123456789abcdef";
std::string s(SHA256_DIGEST_LENGTH * 2, '0');
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
s[2*i] = hex[(out[i] >> 4) & 0xF];
s[2*i + 1] = hex[out[i] & 0xF];
}
return s;
}
} // anonymous namespace
bool DownloadUtxoSnapshot(const std::string& host,
const fs::path& dataDir,
ProgressCallback progressFn,
std::string& strError)
{
const bool noProxy = true;
const char* snapshotFilename = "utxo-snapshot.bin";
// Download utxo-snapshot.bin to a temp file
// Step 1: discover the canonical snapshot filename + expected SHA256 +
// per-snapshot manifest filename from the big manifest.json. Falls back
// to legacy URL if manifest unavailable.
std::string snapshotFilename = "utxo-snapshot.bin";
std::string expectedSha256;
std::string snapshotManifestFilename;
bool haveManifest = false;
fs::path tmpManifest = dataDir / "manifest.json.tmp";
if (DownloadFile(host, "manifest.json", tmpManifest, nullptr, strError, noProxy)) {
std::string text = ReadFileToString(tmpManifest);
fs::remove(tmpManifest);
std::string mFile, mSha, mManifest;
std::string mErr;
if (FindCanonicalSnapshotInManifest(text, mFile, mSha, mManifest, mErr)) {
snapshotFilename = mFile;
expectedSha256 = mSha;
snapshotManifestFilename = mManifest;
haveManifest = true;
printf("Bootstrap: manifest declares canonical snapshot %s (sha256=%s)\n",
snapshotFilename.c_str(), expectedSha256.substr(0, 16).c_str());
} else {
printf("Bootstrap: manifest parse failed (%s) — falling back to legacy URL\n",
mErr.c_str());
}
} else {
printf("Bootstrap: no manifest.json available — falling back to legacy URL\n");
strError.clear();
}
// Step 2: verify the per-snapshot manifest's signature. This is the
// AUTHENTICATION gate — the signature attests that the listed snapshot
// file came from a trusted operator. No checkpoint required; signature
// alone proves authenticity.
if (!snapshotManifestFilename.empty()) {
fs::path tmpSnapManifest = dataDir / "snapshot-manifest.tmp";
if (!DownloadFile(host, snapshotManifestFilename, tmpSnapManifest, nullptr, strError, noProxy)) {
fs::remove(tmpSnapManifest);
return false;
}
std::string snapManifestText = ReadFileToString(tmpSnapManifest);
fs::remove(tmpSnapManifest);
std::string signerAddr = ExtractJsonString(snapManifestText, "signing_address");
std::string message = ExtractJsonString(snapManifestText, "message");
std::string signature = ExtractJsonString(snapManifestText, "signature");
std::string declaredSha = ExtractJsonString(snapManifestText, "snapshot_sha256");
if (signerAddr.empty() || message.empty() || signature.empty()) {
strError = "per-snapshot manifest missing required fields (signing_address/message/signature)";
return false;
}
if (!IsTrustedSnapshotSigner(signerAddr)) {
strError = "snapshot manifest signer " + signerAddr + " is not in trusted signers list";
return false;
}
std::string vErr;
if (!VerifySignedMessage(signerAddr, signature, message, vErr)) {
strError = "snapshot signature verification failed: " + vErr;
return false;
}
if (!declaredSha.empty())
expectedSha256 = declaredSha;
printf("Bootstrap: snapshot signature verified (signer=%s)\n", signerAddr.c_str());
} else {
printf("Bootstrap: WARNING — no per-snapshot manifest available; "
"loading snapshot WITHOUT signature verification\n");
}
// Step 3: download the canonical snapshot file.
fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp";
std::string urlPath = std::string(BASE_PATH) + snapshotFilename;
@@ -790,17 +1101,35 @@ bool DownloadUtxoSnapshot(const std::string& host,
return false;
}
// Step 4: verify the downloaded file's SHA256 against the manifest.
if (!expectedSha256.empty()) {
std::string actualSha = Sha256OfFile(tmpPath);
if (actualSha.empty()) {
strError = "Cannot read downloaded snapshot for SHA256 verification";
fs::remove(tmpPath);
return false;
}
if (actualSha != expectedSha256) {
strError = "Snapshot SHA256 mismatch: expected " + expectedSha256
+ ", got " + actualSha
+ " (manifest/snapshot tampering or server misconfiguration)";
fs::remove(tmpPath);
return false;
}
printf("Bootstrap: snapshot SHA256 verified (%s)\n", actualSha.substr(0, 16).c_str());
}
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
// Load the snapshot into a fresh active chain DB
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) {
// Step 5: load the snapshot. requireCheckpoint is FALSE — signature is
// the authentication gate; checkpoints would force snapshots only at
// specific heights. Signature alone is sufficient.
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/false)) {
fs::remove(tmpPath);
return false;
}
// Clean up the temp file
fs::remove(tmpPath);
printf("Bootstrap: UTXO snapshot loaded successfully.\n");
return true;
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright (c) 2026 The Triangles developers.
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "chaindb_migrate.h"
#include "txdb-leveldb.h"
#include "txdb-rocksdb.h"
#include "util.h"
#include <filesystem>
#include <fstream>
#include <memory>
namespace fs = std::filesystem;
namespace {
struct ChainDbStats
{
int64_t nRecords = 0;
int64_t nUtxos = 0;
int64_t nUtxoValue = 0;
uint256 hashBestChain = 0;
int nDbFormat = 0;
};
bool CollectStats(CTxDBBase& db, ChainDbStats& stats, std::string& strError)
{
stats = ChainDbStats();
auto it = db.NewIterator();
for (it->Seek(std::string()); it->Valid(); it->Next())
stats.nRecords++;
int nUtxos = 0;
stats.nUtxoValue = db.SumUtxoValues(nUtxos);
stats.nUtxos = nUtxos;
db.ReadHashBestChain(stats.hashBestChain);
db.ReadDbFormat(stats.nDbFormat);
if (stats.nRecords <= 0) {
strError = "source chain database contains no records";
return false;
}
return true;
}
bool StatsMatch(const ChainDbStats& src, const ChainDbStats& dst, std::string& strError)
{
if (src.nRecords != dst.nRecords) {
strError = strprintf("record count mismatch after migration: source=%lld rocksdb=%lld",
(long long)src.nRecords, (long long)dst.nRecords);
return false;
}
if (src.nUtxos != dst.nUtxos || src.nUtxoValue != dst.nUtxoValue) {
strError = strprintf("UTXO mismatch after migration: source=(%lld,%lld) rocksdb=(%lld,%lld)",
(long long)src.nUtxos, (long long)src.nUtxoValue,
(long long)dst.nUtxos, (long long)dst.nUtxoValue);
return false;
}
if (src.hashBestChain != dst.hashBestChain) {
strError = strprintf("best-chain hash mismatch after migration: source=%s rocksdb=%s",
src.hashBestChain.ToString().c_str(),
dst.hashBestChain.ToString().c_str());
return false;
}
if (src.nDbFormat != dst.nDbFormat) {
strError = strprintf("dbformat mismatch after migration: source=%d rocksdb=%d",
src.nDbFormat, dst.nDbFormat);
return false;
}
return true;
}
} // namespace
bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
{
strError.clear();
const fs::path dataDir = GetDataDir();
const fs::path levelPath = dataDir / "txleveldb";
const fs::path rocksPath = dataDir / "rocksdb";
const fs::path markerPath = rocksPath / "MIGRATION_INCOMPLETE";
if (!fs::exists(levelPath))
return true;
if (fs::exists(rocksPath)) {
if (fs::exists(markerPath)) {
printf("ChainDB migration: removing incomplete previous RocksDB migration\n");
fs::remove_all(rocksPath);
}
else if (!fForce)
return true;
else {
printf("ChainDB migration: removing existing RocksDB directory due to -migratechaindbforce\n");
fs::remove_all(rocksPath);
}
}
printf("ChainDB migration: copying LevelDB chain state to RocksDB...\n");
printf("ChainDB migration: source=%s destination=%s\n",
levelPath.string().c_str(), rocksPath.string().c_str());
try {
fs::create_directories(rocksPath);
{
std::ofstream marker(markerPath);
marker << "RocksDB migration in progress. Safe to delete this directory and retry.\n";
}
CTxDB source("r");
CRocksTxDB destination("c+");
ChainDbStats srcStats;
if (!CollectStats(source, srcStats, strError)) {
source.Close();
destination.Close();
return false;
}
if (!destination.TxnBegin()) {
strError = "failed to begin RocksDB migration batch";
source.Close();
destination.Close();
return false;
}
int64_t nCopied = 0;
auto it = source.NewIterator();
for (it->Seek(std::string()); it->Valid(); it->Next())
{
if (!destination.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
destination.TxnAbort();
strError = "failed to write migrated record to RocksDB";
source.Close();
destination.Close();
return false;
}
if (++nCopied % 100000 == 0)
{
if (!destination.TxnCommit()) {
strError = "failed to commit RocksDB migration batch";
source.Close();
destination.Close();
return false;
}
printf("ChainDB migration: copied %lld / %lld records\n",
(long long)nCopied, (long long)srcStats.nRecords);
if (!destination.TxnBegin()) {
strError = "failed to begin RocksDB migration batch";
source.Close();
destination.Close();
return false;
}
}
}
if (!destination.TxnCommit()) {
strError = "failed to commit final RocksDB migration batch";
source.Close();
destination.Close();
return false;
}
ChainDbStats dstStats;
if (!CollectStats(destination, dstStats, strError)) {
source.Close();
destination.Close();
return false;
}
if (!StatsMatch(srcStats, dstStats, strError)) {
source.Close();
destination.Close();
return false;
}
printf("ChainDB migration: verified %lld records, %lld UTXOs, best=%s\n",
(long long)dstStats.nRecords,
(long long)dstStats.nUtxos,
dstStats.hashBestChain.ToString().substr(0,20).c_str());
source.Close();
destination.Close();
fs::remove(markerPath);
}
catch (std::exception& e) {
strError = e.what();
return false;
}
printf("ChainDB migration: complete. Legacy LevelDB was left untouched at %s\n",
levelPath.string().c_str());
return true;
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (c) 2026 The Triangles developers.
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_CHAINDB_MIGRATE_H
#define TRIANGLES_CHAINDB_MIGRATE_H
#include <string>
// Migrate legacy LevelDB chain state from <datadir>/txleveldb to RocksDB in
// <datadir>/rocksdb. The source is never modified. Returns true when migration
// succeeds or when there is nothing to migrate.
bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError);
#endif // TRIANGLES_CHAINDB_MIGRATE_H
+6 -18
View File
@@ -32,14 +32,11 @@ namespace Checkpoints
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
{2000000, uint256("0xb0b02d4bb5ffa31f6f22fd042082ca0a085257af34dc2d4b31c3c8567b5574d5")},
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
{2203594, uint256("0x5e016ae5d1f163c6679292b717a3db467a39d24b0a315182f4783caa79c722d8")},
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
// Recent finality pin (PoS era). Closes the long unchecked span from
// 17650 to the live tip so stale-bootstrap / low-trust forks below
// this height are rejected outright. Hash from the canonical chain.
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
@@ -51,7 +48,7 @@ namespace Checkpoints
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{2203594, uint256("0x49b35dd01659975c4a31954f37174c6e2e8878dd0723ab306ccecd991c80f79a")},
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
@@ -70,15 +67,6 @@ namespace Checkpoints
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
{2000000, uint256("0xb0b02d4bb5ffa31f6f22fd042082ca0a085257af34dc2d4b31c3c8567b5574d5")},
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
};
bool CheckHardened(int nHeight, const uint256& hash)
+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 5
#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.
+223
View File
@@ -0,0 +1,223 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#include "hdwallet.h"
#include "bip39_english.h"
#include <cstring>
#include <algorithm>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <secp256k1.h>
namespace hd {
// ---- secp256k1 context (self-contained; independent of crypto_ecdsa) ------
static secp256k1_context* HDContext()
{
static secp256k1_context* ctx = NULL;
if (!ctx)
ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
return ctx;
}
static void HmacSha512(const unsigned char* key, size_t keylen,
const unsigned char* data, size_t datalen,
unsigned char out[64])
{
unsigned int len = 64;
HMAC(EVP_sha512(), key, (int)keylen, data, datalen, out, &len);
}
// Binary search the (lexicographically sorted) BIP39 English wordlist.
static int WordIndex(const std::string& w)
{
int lo = 0, hi = 2047;
while (lo <= hi) {
int mid = (lo + hi) / 2;
int c = w.compare(BIP39_WORDLIST_EN[mid]);
if (c == 0) return mid;
if (c < 0) hi = mid - 1; else lo = mid + 1;
}
return -1;
}
static std::vector<std::string> SplitWords(const std::string& s)
{
std::vector<std::string> out;
size_t i = 0, n = s.size();
while (i < n) {
while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) i++;
size_t j = i;
while (j < n && !(s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r')) j++;
if (j > i) out.push_back(s.substr(i, j - i));
i = j;
}
return out;
}
// ---- BIP39 ----------------------------------------------------------------
std::string GenerateMnemonic(int strengthBits)
{
if (strengthBits != 128 && strengthBits != 256) strengthBits = 256;
int entBytes = strengthBits / 8;
std::vector<unsigned char> ent(entBytes);
if (RAND_bytes(&ent[0], entBytes) != 1) return std::string();
// checksum = first (ENT/32) bits of SHA256(entropy)
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
int csBits = strengthBits / 32;
// bit buffer = entropy || checksum bits
std::vector<unsigned char> bits = ent;
bits.push_back(hash[0]); // up to 8 checksum bits live in hash[0]
int totalBits = strengthBits + csBits;
int words = totalBits / 11;
std::string out;
for (int i = 0; i < words; i++) {
int idx = 0;
for (int b = 0; b < 11; b++) {
int bitpos = i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int bit = (bits[byte] >> off) & 1;
idx = (idx << 1) | bit;
}
if (i) out += ' ';
out += BIP39_WORDLIST_EN[idx];
}
return out;
}
bool CheckMnemonic(const std::string& mnemonic)
{
std::vector<std::string> w = SplitWords(mnemonic);
size_t nw = w.size();
if (nw != 12 && nw != 15 && nw != 18 && nw != 21 && nw != 24) return false;
int totalBits = (int)nw * 11;
int csBits = totalBits / 33;
int entBits = totalBits - csBits;
if (entBits % 8 != 0) return false;
int entBytes = entBits / 8;
// unpack 11-bit indices into a bit buffer
std::vector<unsigned char> buf((totalBits + 7) / 8, 0);
for (size_t i = 0; i < nw; i++) {
int idx = WordIndex(w[i]);
if (idx < 0) return false;
for (int b = 0; b < 11; b++) {
int bit = (idx >> (10 - b)) & 1;
int bitpos = (int)i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
if (bit) buf[byte] |= (1 << off);
}
}
std::vector<unsigned char> ent(buf.begin(), buf.begin() + entBytes);
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
// compare csBits checksum bits
for (int b = 0; b < csBits; b++) {
int bitpos = entBits + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int got = (buf[byte] >> off) & 1;
int want = (hash[b / 8] >> (7 - (b % 8))) & 1;
if (got != want) return false;
}
return true;
}
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64])
{
std::string salt = "mnemonic" + passphrase;
int rc = PKCS5_PBKDF2_HMAC(mnemonic.c_str(), (int)mnemonic.size(),
(const unsigned char*)salt.c_str(), (int)salt.size(),
2048, EVP_sha512(), 64, seed64);
return rc == 1;
}
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out)
{
unsigned char I[64];
HmacSha512((const unsigned char*)"Bitcoin seed", 12, seed, seedlen, I);
memcpy(out.key, I, 32);
memcpy(out.chaincode, I + 32, 32);
if (!secp256k1_ec_seckey_verify(HDContext(), out.key)) return false;
out.valid = true;
return true;
}
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child)
{
if (!parent.valid) return false;
secp256k1_context* ctx = HDContext();
unsigned char data[37];
size_t dlen = 0;
if (index & HARDENED) {
data[0] = 0x00;
memcpy(data + 1, parent.key, 32);
dlen = 33;
} else {
// serP(point(parent.key)) = 33-byte compressed pubkey
secp256k1_pubkey pk;
if (!secp256k1_ec_pubkey_create(ctx, &pk, parent.key)) return false;
size_t plen = 33;
secp256k1_ec_pubkey_serialize(ctx, data, &plen, &pk, SECP256K1_EC_COMPRESSED);
dlen = 33;
}
data[dlen + 0] = (index >> 24) & 0xff;
data[dlen + 1] = (index >> 16) & 0xff;
data[dlen + 2] = (index >> 8) & 0xff;
data[dlen + 3] = index & 0xff;
dlen += 4;
unsigned char I[64];
HmacSha512(parent.chaincode, 32, data, dlen, I);
memcpy(child.key, parent.key, 32);
// child = (IL + parent) mod n ; rejects invalid (IL>=n or result 0)
if (!secp256k1_ec_seckey_tweak_add(ctx, child.key, I)) return false;
memcpy(child.chaincode, I + 32, 32);
child.valid = true;
return true;
}
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out)
{
ExtKey cur = master;
for (size_t i = 0; i < path.size(); i++) {
ExtKey nxt;
if (!CKDpriv(cur, path[i], nxt)) return false;
cur = nxt;
}
out = cur;
return true;
}
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32])
{
unsigned char seed[64];
if (!MnemonicToSeed(mnemonic, passphrase, seed)) return false;
ExtKey master;
if (!MasterFromSeed(seed, 64, master)) return false;
std::vector<uint32_t> path;
path.push_back(44u | HARDENED);
path.push_back(TRI_COIN_TYPE | HARDENED);
path.push_back(account | HARDENED);
path.push_back(change);
path.push_back(index);
ExtKey leaf;
if (!DerivePath(master, path, leaf)) return false;
memcpy(privOut, leaf.key, 32);
return true;
}
} // namespace hd
+50
View File
@@ -0,0 +1,50 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
//
// Native BIP39 (mnemonic) + BIP32 (HD) key derivation for Triangles.
// Produces keys identical to the TRIdock web wallet (derivation path
// m/44'/2222'/0'/0/i, coin type 2222), so a 24-word phrase round-trips
// between the Qt/daemon wallet and the web wallet.
#ifndef TRIANGLES_HDWALLET_H
#define TRIANGLES_HDWALLET_H
#include <string>
#include <vector>
#include <cstdint>
#include <cstddef>
namespace hd {
static const uint32_t HARDENED = 0x80000000u;
static const uint32_t TRI_COIN_TYPE = 2222u; // matches triWallet.js
// A BIP32 extended private key (private scalar + chain code).
struct ExtKey {
unsigned char key[32];
unsigned char chaincode[32];
bool valid;
ExtKey() : valid(false) { }
};
// ---- BIP39 ----------------------------------------------------------------
// Generate a new mnemonic. strengthBits must be 128 (12 words) or 256 (24).
std::string GenerateMnemonic(int strengthBits = 256);
// Validate word membership + checksum.
bool CheckMnemonic(const std::string& mnemonic);
// PBKDF2-HMAC-SHA512(mnemonic, "mnemonic"+passphrase, 2048) -> 64-byte seed.
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64]);
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out);
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child);
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out);
// ---- High level -----------------------------------------------------------
// Derive the 32-byte private scalar for m/44'/coinType'/account'/change/index.
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32]);
} // namespace hd
#endif // TRIANGLES_HDWALLET_H
+177 -21
View File
@@ -24,12 +24,14 @@
#endif
#include "notificationqueue.h"
#include "addressindex.h"
#include "chaindb_migrate.h"
#include <memory>
#include <thread>
#include <vector>
#include <filesystem>
#include <fstream>
#include <boost/interprocess/sync/file_lock.hpp>
#include <algorithm>
#include <openssl/crypto.h>
#ifndef WIN32
@@ -101,6 +103,97 @@ void ExitTimeout(void* parg)
#endif
}
// Wait up to maxWaitSec for at least minPeers peers to have reported their
// chain height via the version handshake. Returns the median peer height, or
// -1 if we couldn't get enough peers (timeout, no peers, all nStartingHeight=-1).
int WaitForPeerHeights(int minPeers, int maxWaitSec)
{
const int pollIntervalMs = 500;
const int64_t deadline = GetTimeMillis() + (int64_t)maxWaitSec * 1000;
while (GetTimeMillis() < deadline && !fRequestShutdown) {
std::vector<int> heights;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode && pnode->nStartingHeight > 0)
heights.push_back(pnode->nStartingHeight);
}
}
if ((int)heights.size() >= minPeers) {
std::sort(heights.begin(), heights.end());
int median = heights[heights.size() / 2];
printf("AutoRebuild: got %zu peer heights; median=%d\n", heights.size(), median);
return median;
}
MilliSleep(pollIntervalMs);
}
std::vector<int> heights;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode && pnode->nStartingHeight > 0)
heights.push_back(pnode->nStartingHeight);
}
}
if (heights.empty()) {
printf("AutoRebuild: no peers reported heights after %ds\n", maxWaitSec);
return -1;
}
std::sort(heights.begin(), heights.end());
int median = heights[heights.size() / 2];
printf("AutoRebuild: timed out with %zu peers; median=%d\n", heights.size(), median);
return median;
}
// If -autorerebuild is set and our local chain is more than that many blocks
// behind the median peer height, wipe the chain DB (preserving wallet.dat +
// onion + smsg state) and request shutdown. On restart, the daemon sees no
// chain DB and the snapshot path takes over.
void MaybeAutoRebuild(int thresholdBlocks)
{
if (thresholdBlocks <= 0)
return;
if (nBestHeight < 0) {
printf("AutoRebuild: local nBestHeight unset — skipping\n");
return;
}
printf("AutoRebuild: enabled (threshold=%d blocks). Local chain tip: %d\n",
thresholdBlocks, nBestHeight);
int medianPeer = WaitForPeerHeights(/*minPeers=*/3, /*maxWaitSec=*/60);
if (medianPeer <= 0) {
printf("AutoRebuild: could not get peer heights — skipping rebuild\n");
return;
}
int lag = medianPeer - nBestHeight;
printf("AutoRebuild: peer median=%d, local=%d, lag=%d\n",
medianPeer, nBestHeight, lag);
if (lag < thresholdBlocks) {
printf("AutoRebuild: lag %d < threshold %d — no rebuild needed\n",
lag, thresholdBlocks);
return;
}
printf("\n*** AutoRebuild: chain is %d blocks behind — wiping chain DB ***\n", lag);
printf("*** Preserving wallet.dat, smsgDB, onion state. ***\n");
printf("*** Daemon will shutdown; restart to load signed UTXO snapshot. ***\n\n");
WipeChainDataDir();
fs::path blkPath = GetDataDir() / "blk0001.dat";
if (fs::exists(blkPath)) {
fs::remove(blkPath);
printf("AutoRebuild: removed stale %s\n", blkPath.string().c_str());
}
StartShutdown();
}
void StartShutdown()
{
fRequestShutdown = true;
@@ -416,7 +509,7 @@ std::string HelpMessage()
//" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" +
//" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" +
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
" -notor " + _("Disable Tor (WARNING: wallet will not start - Tor is required)") + "\n" +
" -notor " + _("Disable Tor - run in clearnet-only mode (no .onion connectivity)") + "\n" +
" -torsocks=<port> " + _("Set embedded or managed Tor SOCKS proxy port (default: 19099)") + "\n" +
" -torhiddenservice " + _("Enable the managed Tor hidden service (default: 1)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" +
@@ -439,6 +532,7 @@ std::string HelpMessage()
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
" -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" +
" -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" +
@@ -926,7 +1020,8 @@ bool AppInit2()
// v5.9.5: P2P UTXO snapshot fetch is the default for fresh installs (Step 11.6).
// The legacy clearnet HTTP bootstrap only runs when the user explicitly requests
// it via -bootstrap, or when -snapshot=0 disables the P2P fetcher.
#ifndef QT_GUI
// Bootstrap auto-download works for both GUI and daemon.
// GUI users get the same automatic bootstrap on fresh installs.
{
bool wantsBootstrap = GetBoolArg("-bootstrap", false);
bool noBootstrap = GetBoolArg("-nobootstrap", false);
@@ -934,13 +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 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)
@@ -950,13 +1043,24 @@ bool AppInit2()
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
auto progressFn = [](int64_t bytesDownloaded, int64_t totalBytes) {
int64_t lastGuiUpdate = 0;
auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) {
if (totalBytes > 0) {
printf("\rBootstrap: %lld / %lld MB (%lld%%)",
(long long)(bytesDownloaded / (1024*1024)),
(long long)(totalBytes / (1024*1024)),
(long long)((bytesDownloaded * 100) / totalBytes));
fflush(stdout);
// Update GUI status bar every ~1 MB
int64_t now = GetTimeMillis();
if (now - lastGuiUpdate > 1000) {
lastGuiUpdate = now;
std::string msg = strprintf("Downloading blockchain: %lld / %lld MB (%lld%%)",
(long long)(bytesDownloaded / (1024*1024)),
(long long)(totalBytes / (1024*1024)),
(long long)((bytesDownloaded * 100) / totalBytes));
uiInterface.InitMessage(msg);
}
}
};
@@ -998,7 +1102,6 @@ bool AppInit2()
strprintf("host=%s success=%d utxo_snapshot=%d", host.c_str(), success, triedUtxoSnapshot));
}
} // end bootstrap scope
#endif
// ********************************************************* Step 6c: manual UTXO snapshot loading
// If utxo-snapshot.bin exists in data dir and the chain DB hasn't been
@@ -1012,8 +1115,13 @@ bool AppInit2()
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
// Local file load: skip the checkpoint gate. The operator has
// filesystem access, so the trust model is already equivalent
// to direct chain state modification — a malicious local file
// is no worse than a malicious chain DB. P2P-delivered
// snapshots (SnapshotNet) keep the checkpoint gate on.
std::string strError;
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) {
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) {
printf("UTXO snapshot loaded successfully.\n");
} else {
printf("UTXO snapshot load failed: %s\n", strError.c_str());
@@ -1022,6 +1130,16 @@ bool AppInit2()
}
}
// ********************************************************* Step 6d: optional LevelDB -> RocksDB chain DB migration
if (GetBoolArg("-migratechaindb", false) || GetBoolArg("-migratechaindbforce", false))
{
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
std::string strMigrateError;
bool fForce = GetBoolArg("-migratechaindbforce", false);
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
}
// ********************************************************* Step 7: load blockchain
if (!bitdb.Open(GetDataDir()))
@@ -1041,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.
@@ -1058,19 +1176,48 @@ bool AppInit2()
if (!LoadBlockIndex())
return InitError(_("Error loading blkindex.dat"));
// If the block index is empty but blk0001.dat exists (bootstrap download),
// fast-import: build the index directly from the block file without re-writing
// data. Batches LevelDB commits every 200K blocks for speed.
if (nBestHeight == 0 && std::filesystem::exists(GetDataDir() / "blk0001.dat")
&& mapBlockIndex.size() <= 1)
// triangles fix (pitfall #61): initialize pindexFinalized from the
// hardcoded checkpoint on startup, BEFORE the daemon opens any peer
// connections or processes any block messages.
//
// Without this, pindexFinalized stays NULL on a fresh restart even when
// we have 2.2M blocks on disk, because the auto-checkpoint code in
// ActivateBestChain() at main.cpp:2459 only sets it when
// !IsInitialBlockDownload(). If the chain tip is more than 24h stale
// (which happens on every restart with a synced chain), IsInitialBlockDownload()
// returns true and pindexFinalized never gets set.
//
// The downstream reorg guard at main.cpp:2198 short-circuits when
// pindexFinalized is NULL, which allowed a 3,755-block minority fork
// to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading
// the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on
// startup means the reorg guard is always active whenever the
// checkpointed block is in our local mapBlockIndex.
{
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));
CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
if (pCheckpoint && pCheckpoint != pindexFinalized)
{
pindexFinalized = pCheckpoint;
printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n",
pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
}
else if (!pCheckpoint)
{
printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n");
}
}
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
// and shutdown for clean restart.
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
if (fRequestShutdown) {
printf("AutoRebuild: shutdown requested before chain load complete\n");
return false;
}
// 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.
// As the program has not fully started yet, Shutdown() is possibly overkill.
@@ -1332,6 +1479,15 @@ bool AppInit2()
#ifdef USE_UPNP
fUseUPnP = false;
#endif
} else if (GetBoolArg("-notor", false)) {
// -notor: user explicitly disabled Tor. Allow the daemon to start
// in clearnet-only mode (useful for diagnostics, benchmarking, and
// recovery). .onion connectivity will not be available.
printf("NOTICE: Tor disabled via -notor. Running in clearnet-only mode.\n");
printf(" .onion connections will NOT be available.\n");
SetReachable(NET_IPV4, true);
SetReachable(NET_IPV6, true);
SetReachable(NET_TOR, false);
} else {
std::string torError = CTorEmbedded::GetInstance()->GetStartupError();
if (torError.empty())
+11 -1
View File
@@ -36,8 +36,18 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
// comes back with massively amplified staking power and dominates blocks.
// The 7-day cap still allows generous accumulation while limiting abuse.
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
// Activation gate: the soft cap shipped 2026-04-20 without a height/time
// gate, retroactively invalidating earlier blocks staked with long-aged
// coins (e.g. coins idle through the 2022-2026 freeze). Apply the cap
// only to stakes after the activation timestamp; historical stakes
// validate under the rules they were created with (uncapped age).
static const int64_t STAKE_AGE_SOFT_CAP_ACTIVATION = 1776000000; // 2026-04-12 ~13:20 UTC
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
return min(nAge, STAKE_AGE_SOFT_CAP);
{
if (nIntervalEnd >= STAKE_AGE_SOFT_CAP_ACTIVATION)
return min(nAge, STAKE_AGE_SOFT_CAP);
return nAge;
}
return min(nAge, (int64_t)nStakeMaxAge);
}
+141 -1003
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -83,6 +83,7 @@ extern unsigned int nStakeMinAge;
extern unsigned int nNodeLifespan;
extern int nCoinbaseMaturity;
extern int nBestHeight;
extern bool fLoadedFromSnapshot; // true after successful UtxoSnapshot::LoadSnapshot
extern uint256 nBestChainTrust;
extern uint256 nBestInvalidTrust;
extern uint256 hashBestChain;
@@ -128,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);
+1 -1
View File
@@ -545,7 +545,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog)
int64_t nFee = nTransactionFee * (1 + (int64_t)nBytes / 1000);
// Min Fee
int64_t nMinFee = txDummy.GetMinFee(1, GMF_SEND, nBytes);
int64_t nMinFee = txDummy.GetMinFee(1, GetMinFeeMode::Send, nBytes);
nPayFee = max(nFee, nMinFee);
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#include "hdseeddialog.h"
#include "walletmodel.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QLabel>
#include <QMessageBox>
#include <QFont>
HDSeedDialog::HDSeedDialog(QWidget *parent)
: QDialog(parent), model(0), seedText(0), statusLabel(0)
{
setWindowTitle(tr("HD Seed Phrase (BIP39)"));
resize(560, 360);
QVBoxLayout *layout = new QVBoxLayout(this);
QLabel *intro = new QLabel(tr(
"A 24-word seed phrase is a complete backup of this wallet. Anyone who has it "
"can spend your coins. Write it down on paper and keep it offline."), this);
intro->setWordWrap(true);
layout->addWidget(intro);
seedText = new QPlainTextEdit(this);
seedText->setPlaceholderText(tr(
"Your 24-word phrase appears here when you generate or reveal it. "
"To restore, paste an existing 24-word phrase here and click 'Restore from Phrase'."));
QFont mono("monospace");
mono.setStyleHint(QFont::Monospace);
seedText->setFont(mono);
layout->addWidget(seedText);
statusLabel = new QLabel(this);
statusLabel->setWordWrap(true);
layout->addWidget(statusLabel);
QHBoxLayout *btns = new QHBoxLayout();
QPushButton *genBtn = new QPushButton(tr("Generate New"), this);
QPushButton *showBtn = new QPushButton(tr("Reveal for Backup"), this);
QPushButton *restoreBtn = new QPushButton(tr("Restore from Phrase"), this);
QPushButton *closeBtn = new QPushButton(tr("Close"), this);
btns->addWidget(genBtn);
btns->addWidget(showBtn);
btns->addWidget(restoreBtn);
btns->addStretch();
btns->addWidget(closeBtn);
layout->addLayout(btns);
connect(genBtn, SIGNAL(clicked()), this, SLOT(onGenerate()));
connect(showBtn, SIGNAL(clicked()), this, SLOT(onShow()));
connect(restoreBtn, SIGNAL(clicked()), this, SLOT(onRestore()));
connect(closeBtn, SIGNAL(clicked()), this, SLOT(accept()));
}
void HDSeedDialog::setModel(WalletModel *modelIn)
{
model = modelIn;
refreshStatus();
}
void HDSeedDialog::refreshStatus()
{
if (!model || !statusLabel) return;
if (model->hdEnabled())
statusLabel->setText(tr("Status: HD seed is ACTIVE. Use 'Reveal for Backup' to view your phrase."));
else
statusLabel->setText(tr("Status: no HD seed yet. Use 'Generate New' to create one."));
}
void HDSeedDialog::onGenerate()
{
if (!model) return;
if (model->hdEnabled()) {
QMessageBox::warning(this, tr("HD seed already set"),
tr("This wallet already has an HD seed. Use 'Reveal for Backup' to view it."));
return;
}
if (QMessageBox::question(this, tr("Generate new seed"),
tr("Generate a new 24-word HD seed for this wallet?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString mnemonic, err;
if (!model->hdNew(mnemonic, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
seedText->setPlainText(mnemonic);
QMessageBox::information(this, tr("Write this down"),
tr("Your new 24-word seed phrase is shown above. Write it on paper and store it safely "
"and offline. This is the only backup of this wallet."));
refreshStatus();
}
void HDSeedDialog::onShow()
{
if (!model) return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString mnemonic, err;
if (!model->hdShow(mnemonic, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
seedText->setPlainText(mnemonic);
}
void HDSeedDialog::onRestore()
{
if (!model) return;
QString phrase = seedText->toPlainText().trimmed();
if (phrase.isEmpty()) {
QMessageBox::warning(this, tr("No phrase"),
tr("Paste a 24-word phrase into the box first."));
return;
}
if (QMessageBox::question(this, tr("Restore from phrase"),
tr("Restore the HD seed from the phrase in the box and rescan the chain? "
"This replaces the current HD seed."),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString err;
if (!model->hdRestore(phrase, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
QMessageBox::information(this, tr("Restored"),
tr("HD seed restored and the chain was rescanned for your funds."));
refreshStatus();
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#ifndef HDSEEDDIALOG_H
#define HDSEEDDIALOG_H
#include <QDialog>
class WalletModel;
QT_BEGIN_NAMESPACE
class QPlainTextEdit;
class QLabel;
QT_END_NAMESPACE
/** Generate, reveal (for backup), and restore the wallet's BIP39 HD seed phrase. */
class HDSeedDialog : public QDialog
{
Q_OBJECT
public:
explicit HDSeedDialog(QWidget *parent = 0);
void setModel(WalletModel *model);
private:
WalletModel *model;
QPlainTextEdit *seedText;
QLabel *statusLabel;
void refreshStatus();
private slots:
void onGenerate();
void onShow();
void onRestore();
};
#endif // HDSEEDDIALOG_H
Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+19
View File
@@ -31,6 +31,7 @@
#include "trianglesunits.h"
#include "guiconstants.h"
#include "askpassphrasedialog.h"
#include "hdseeddialog.h"
#include "notificator.h"
#include "guiutil.h"
#include "rpcconsole.h"
@@ -484,6 +485,8 @@ void TrianglesGUI::createActions(bool fIsTestnet)
backupWalletAction->setStatusTip(tr("Backup wallet to another location"));
changePassphraseAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase..."), this);
changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption"));
hdSeedAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Seed Phrase (HD Backup)..."), this);
hdSeedAction->setStatusTip(tr("Generate, restore, or back up your 24-word HD seed phrase"));
unlockWalletAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet..."), this);
unlockWalletAction->setStatusTip(tr("Unlock wallet"));
unlockWalletStakingAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet for staking..."), this);
@@ -509,6 +512,7 @@ void TrianglesGUI::createActions(bool fIsTestnet)
connect(encryptWalletAction, SIGNAL(triggered(bool)), this, SLOT(encryptWallet(bool)));
connect(backupWalletAction, SIGNAL(triggered()), this, SLOT(backupWallet()));
connect(changePassphraseAction, SIGNAL(triggered()), this, SLOT(changePassphrase()));
connect(hdSeedAction, SIGNAL(triggered()), this, SLOT(hdSeedManager()));
connect(unlockWalletAction, SIGNAL(triggered()), this, SLOT(unlockWallet()));
connect(unlockWalletStakingAction, SIGNAL(triggered()), this, SLOT(unlockWalletStaking()));
connect(lockWalletAction, SIGNAL(triggered()), this, SLOT(lockWallet()));
@@ -538,6 +542,7 @@ void TrianglesGUI::createMenuBar()
QMenu *settings = appMenuBar->addMenu(tr("&Settings"));
settings->addAction(encryptWalletAction);
settings->addAction(changePassphraseAction);
settings->addAction(hdSeedAction);
settings->addAction(unlockWalletAction);
settings->addAction(lockWalletAction);
settings->addSeparator();
@@ -1448,6 +1453,7 @@ void TrianglesGUI::menuOperationsRequested()
QAction* unlockWalletStaking = menu.addAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet...").remove('&').remove("..."));
QAction* lockWallet = menu.addAction(QIcon(":/menu_16/lock"), tr("&Lock Wallet...").remove('&').remove("..."));
QAction* changePassword = menu.addAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase...").remove('&').remove("..."));
QAction* hdSeed = menu.addAction(QIcon(":/menu_16/passphrase"), tr("Seed Phrase (HD Backup)..."));
QAction* signMessage = menu.addAction(QIcon(":/menu_16/sign"), tr("Sign &message...").remove('&').remove("..."));
QAction* verifySignature = menu.addAction(QIcon(":/menu_16/verify"), tr("&Verify message...").remove('&').remove("..."));
@@ -1508,6 +1514,10 @@ void TrianglesGUI::menuOperationsRequested()
if (walletModel->getEncryptionStatus() == WalletModel::Unlocked || walletModel->getEncryptionStatus() == WalletModel::Locked)
changePassphrase();
}
else if (selected == hdSeed)
{
hdSeedManager();
}
else if (selected == signMessage)
{
gotoSignMessageTab();
@@ -1614,6 +1624,15 @@ void TrianglesGUI::changePassphrase()
dlg.exec();
}
void TrianglesGUI::hdSeedManager()
{
if (!walletModel)
return;
HDSeedDialog dlg(this);
dlg.setModel(walletModel);
dlg.exec();
}
void TrianglesGUI::unlockWalletStaking()
{
+3
View File
@@ -131,6 +131,7 @@ private:
QAction *encryptWalletAction;
QAction *backupWalletAction;
QAction *changePassphraseAction;
QAction *hdSeedAction;
QAction *unlockWalletAction;
QAction *unlockWalletStakingAction;
QAction *lockWalletAction;
@@ -243,6 +244,8 @@ private slots:
void backupWallet();
/** Change encrypted wallet passphrase */
void changePassphrase();
/** Open the HD seed phrase (generate/restore/backup) dialog */
void hdSeedManager();
/** Ask for passphrase to unlock wallet temporarily */
void unlockWallet();
/** Ask for passphrase to unlock wallet temporarily - FOR STAKING ONLY */
+46
View File
@@ -676,3 +676,49 @@ void WalletModel::listLockedCoins(std::vector<COutPoint>& vOutpts)
{
return;
}
// ---- HD wallet (BIP39/BIP32) ----
bool WalletModel::hdEnabled() const
{
return wallet->IsHDEnabled();
}
bool WalletModel::hdNew(QString &mnemonicOut, QString &errorOut)
{
std::string mnemonic, strError;
if (!wallet->SetHDSeed("", "", true, mnemonic, strError)) {
errorOut = QString::fromStdString(strError);
return false;
}
wallet->TopUpKeyPool();
mnemonicOut = QString::fromStdString(mnemonic);
return true;
}
bool WalletModel::hdRestore(const QString &mnemonic, QString &errorOut)
{
std::string out, strError;
if (!wallet->SetHDSeed(mnemonic.toStdString(), "", false, out, strError)) {
errorOut = QString::fromStdString(strError);
return false;
}
wallet->TopUpKeyPool();
{
LOCK2(cs_main, wallet->cs_wallet);
wallet->ScanForWalletTransactions(pindexGenesisBlock, true);
wallet->ReacceptWalletTransactions();
}
return true;
}
bool WalletModel::hdShow(QString &mnemonicOut, QString &errorOut)
{
std::string mnemonic;
if (!wallet->GetHDMnemonic(mnemonic)) {
errorOut = QObject::tr("Wallet has no HD seed (use 'Generate New').");
return false;
}
mnemonicOut = QString::fromStdString(mnemonic);
return true;
}
+6
View File
@@ -103,6 +103,12 @@ public:
// Wallet backup
bool backupWallet(const QString &filename);
// ---- HD wallet (BIP39/BIP32) ----
bool hdEnabled() const;
bool hdNew(QString &mnemonicOut, QString &errorOut);
bool hdRestore(const QString &mnemonic, QString &errorOut);
bool hdShow(QString &mnemonicOut, QString &errorOut);
// RAI object for unlocking wallet, returned by requestUnlock()
class UnlockContext
{
+75
View File
@@ -1858,3 +1858,78 @@ Value makekeypair(const Array& params, bool fHelp)
result.push_back(Pair("PublicKey", HexStr(key.GetPubKey().Raw())));
return result;
}
Value hdinfo(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error("hdinfo\nReturns HD (BIP39/BIP32) wallet status.");
Object obj;
obj.push_back(Pair("hdenabled", pwalletMain->IsHDEnabled()));
obj.push_back(Pair("coin_type", 2222));
obj.push_back(Pair("derivation_path", "m/44'/2222'/0'/0/i"));
obj.push_back(Pair("nextindex", (int64_t)pwalletMain->nHDChainIndex));
return obj;
}
Value hdnew(const Array& params, bool fHelp)
{
if (fHelp || params.size() > 1)
throw runtime_error(
"hdnew [passphrase]\n"
"Generate a NEW 24-word HD seed phrase, activate it as this wallet's\n"
"deterministic seed, and return the phrase. WRITE IT DOWN: it is the\n"
"only backup of every address this wallet derives.");
EnsureWalletIsUnlocked();
if (pwalletMain->IsHDEnabled())
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet already has an HD seed; use 'hdshow' to back it up.");
string passphrase = params.size() > 0 ? params[0].get_str() : "";
string mnemonic, strError;
if (!pwalletMain->SetHDSeed("", passphrase, true, mnemonic, strError))
throw JSONRPCError(RPC_WALLET_ERROR, strError);
pwalletMain->TopUpKeyPool();
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("words", 24));
obj.push_back(Pair("warning", "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."));
return obj;
}
Value hdrestore(const Array& params, bool fHelp)
{
if (fHelp || params.size() < 1 || params.size() > 2)
throw runtime_error(
"hdrestore \"mnemonic\" [passphrase]\n"
"Activate an HD seed from an existing 24-word phrase, derive the keypool\n"
"and rescan the chain for funds on the derived addresses.");
EnsureWalletIsUnlocked();
string mnemonic = params[0].get_str();
string passphrase = params.size() > 1 ? params[1].get_str() : "";
string out, strError;
if (!pwalletMain->SetHDSeed(mnemonic, passphrase, false, out, strError))
throw JSONRPCError(RPC_WALLET_ERROR, strError);
pwalletMain->TopUpKeyPool();
{
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
pwalletMain->ReacceptWalletTransactions();
}
Object obj;
obj.push_back(Pair("restored", true));
return obj;
}
Value hdshow(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"hdshow\nReveal the wallet's HD mnemonic for backup. The wallet must be unlocked.");
EnsureWalletIsUnlocked();
string mnemonic;
if (!pwalletMain->GetHDMnemonic(mnemonic))
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet has no HD seed (use 'hdnew' to create one).");
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("warning", "Keep these words secret and offline."));
return obj;
}
+664
View File
@@ -0,0 +1,664 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "syncmanager.h"
#include "bignum.h"
#include "checkpoints.h"
#include "main.h"
#include "net.h"
#include "util.h"
#include <algorithm>
#include <map>
struct CSyncManager::HeaderNode
{
CBlock header;
int nHeight;
uint256 nChainTrust;
bool fRequested;
int64_t nLastRequestTime;
int64_t nFirstRequestTime;
int64_t nInsertTime;
};
namespace
{
static const unsigned int MAX_HEADER_SYNC_CACHE = 15000;
static const size_t HEADER_REDUNDANT_PEER_THRESHOLD = 4;
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 60 * 1000000;
static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 5 * 1000000;
static const int64_t HEADER_SYNC_TTL_MICROS = 15 * 60 * 1000000;
std::map<uint256, CSyncManager::HeaderNode> mapHeaders;
uint256 hashBestHeader = 0;
int64_t nLastNewHeaderTime = 0;
}
CSyncManager g_syncManager;
bool CSyncManager::HaveHeader(const uint256& hash) const
{
return mapHeaders.count(hash) != 0;
}
uint256 CSyncManager::GetBestHeader() const
{
return hashBestHeader;
}
std::size_t CSyncManager::GetHeaderCount() const
{
return mapHeaders.size();
}
uint256 CSyncManager::GetHeaderTrust(unsigned int nBits) const
{
CBigNum bnTarget;
bnTarget.SetCompact(nBits);
if (bnTarget <= 0)
return 0;
return ((CBigNum(1) << 256) / (bnTarget + 1)).getuint256();
}
bool CSyncManager::GetKnownHeaderState(const uint256& hash, int& nHeight, uint256& nChainTrust) const
{
std::map<uint256, CBlockIndex*>::const_iterator miBlock = mapBlockIndex.find(hash);
if (miBlock != mapBlockIndex.end())
{
nHeight = miBlock->second->nHeight;
nChainTrust = miBlock->second->nChainTrust;
return true;
}
std::map<uint256, HeaderNode>::const_iterator miHeader = mapHeaders.find(hash);
if (miHeader != mapHeaders.end())
{
nHeight = miHeader->second.nHeight;
nChainTrust = miHeader->second.nChainTrust;
return true;
}
return false;
}
bool CSyncManager::GetPrevHash(const uint256& hash, uint256& hashPrev) const
{
std::map<uint256, HeaderNode>::const_iterator miHeader = mapHeaders.find(hash);
if (miHeader != mapHeaders.end())
{
hashPrev = miHeader->second.header.hashPrevBlock;
return true;
}
std::map<uint256, CBlockIndex*>::const_iterator miBlock = mapBlockIndex.find(hash);
if (miBlock != mapBlockIndex.end() && miBlock->second->pprev)
{
hashPrev = miBlock->second->pprev->GetBlockHash();
return true;
}
return false;
}
void CSyncManager::RecomputeBestHeader()
{
hashBestHeader = 0;
uint256 nBestTrust = 0;
for (std::map<uint256, HeaderNode>::const_iterator it = mapHeaders.begin(); it != mapHeaders.end(); ++it)
{
if (hashBestHeader == 0 || it->second.nChainTrust > nBestTrust)
{
hashBestHeader = it->first;
nBestTrust = it->second.nChainTrust;
}
}
}
void CSyncManager::PruneHeaders()
{
const int64_t nNow = GetTime() * 1000000;
if (mapHeaders.size() > MAX_HEADER_SYNC_CACHE / 2)
{
unsigned int nEvicted = 0;
for (std::map<uint256, HeaderNode>::iterator it = mapHeaders.begin(); it != mapHeaders.end(); )
{
if (nNow - it->second.nInsertTime >= HEADER_SYNC_TTL_MICROS)
{
it = mapHeaders.erase(it);
++nEvicted;
}
else
++it;
}
if (nEvicted > 0)
{
printf("IBD-DIAG: TTL-evicted %u stale sync headers, %u remain\n",
nEvicted, (unsigned int)mapHeaders.size());
RecomputeBestHeader();
}
}
if (mapHeaders.size() > MAX_HEADER_SYNC_CACHE)
{
printf("IBD-DIAG: sync header cache exceeded %u entries, evicting oldest\n", MAX_HEADER_SYNC_CACHE);
while (mapHeaders.size() > MAX_HEADER_SYNC_CACHE * 3 / 4)
{
std::map<uint256, HeaderNode>::iterator oldest = mapHeaders.begin();
for (std::map<uint256, HeaderNode>::iterator it = mapHeaders.begin(); it != mapHeaders.end(); ++it)
{
if (it->second.nInsertTime < oldest->second.nInsertTime)
oldest = it;
}
mapHeaders.erase(oldest);
}
RecomputeBestHeader();
}
}
bool CSyncManager::AddHeaderNode(const CBlock& header, const uint256& hashHeader)
{
if (mapBlockIndex.count(hashHeader) || mapHeaders.count(hashHeader))
return true;
if (!header.vtx.empty())
{
printf("IBD-DIAG: header rejected (has vtx) hash=%s\n", hashHeader.ToString().substr(0,20).c_str());
return false;
}
if (header.GetBlockTime() > GetTime() + 15 * 60)
{
printf("IBD-DIAG: header rejected (future time) hash=%s time=%u\n",
hashHeader.ToString().substr(0,20).c_str(), header.nTime);
return false;
}
int nPrevHeight = -1;
uint256 nPrevChainTrust = 0;
if (!GetKnownHeaderState(header.hashPrevBlock, nPrevHeight, nPrevChainTrust))
{
printf("IBD-DIAG: header rejected (prev unknown) hash=%s prevHash=%s\n",
hashHeader.ToString().substr(0,20).c_str(),
header.hashPrevBlock.ToString().substr(0,20).c_str());
return false;
}
const int nHeight = nPrevHeight + 1;
if (nHeight <= CUTOFF_POW_BLOCK && !CheckProofOfWork(hashHeader, header.nBits))
{
printf("IBD-DIAG: header PoW FAILED at height %d hash=%s nBits=%08x prevHash=%s\n",
nHeight, hashHeader.ToString().substr(0,20).c_str(), header.nBits,
header.hashPrevBlock.ToString().substr(0,20).c_str());
return false;
}
HeaderNode node;
node.header = header;
node.nHeight = nHeight;
node.nChainTrust = nPrevChainTrust + GetHeaderTrust(header.nBits);
node.fRequested = false;
node.nLastRequestTime = 0;
node.nFirstRequestTime = 0;
node.nInsertTime = GetTime() * 1000000;
mapHeaders.insert({hashHeader, node});
if (hashBestHeader == 0 || node.nChainTrust > mapHeaders[hashBestHeader].nChainTrust)
hashBestHeader = hashHeader;
PruneHeaders();
return true;
}
std::vector<uint256> CSyncManager::GetDownloadPath(uint256 hashTip) const
{
std::vector<uint256> vPath;
while (hashTip != 0 && !mapBlockIndex.count(hashTip))
{
std::map<uint256, HeaderNode>::const_iterator mi = mapHeaders.find(hashTip);
if (mi == mapHeaders.end())
break;
vPath.push_back(hashTip);
hashTip = mi->second.header.hashPrevBlock;
}
std::reverse(vPath.begin(), vPath.end());
return vPath;
}
unsigned int CSyncManager::CountInFlight() const
{
const int64_t nNow = GetTime() * 1000000;
unsigned int nInFlight = 0;
for (std::map<uint256, HeaderNode>::const_iterator it = mapHeaders.begin(); it != mapHeaders.end(); ++it)
{
if (it->second.fRequested && nNow - it->second.nLastRequestTime < HEADER_REQUEST_TIMEOUT_MICROS)
++nInFlight;
}
return nInFlight;
}
unsigned int CSyncManager::GetPlannerDepth() const
{
if (hashBestHeader == 0)
return 0;
return (unsigned int)GetDownloadPath(hashBestHeader).size();
}
int CSyncManager::GetPlannerHeight() const
{
if (hashBestHeader == 0)
return pindexBest ? pindexBest->nHeight : -1;
std::map<uint256, HeaderNode>::const_iterator mi = mapHeaders.find(hashBestHeader);
if (mi == mapHeaders.end())
return pindexBest ? pindexBest->nHeight : -1;
return mi->second.nHeight;
}
int64_t CSyncManager::GetRequestTime(const uint256& hashBlock) const
{
std::map<uint256, HeaderNode>::const_iterator mi = mapHeaders.find(hashBlock);
if (mi == mapHeaders.end())
return 0;
return mi->second.nFirstRequestTime;
}
void CSyncManager::BlockAccepted(const uint256& hashBlock)
{
std::map<uint256, HeaderNode>::iterator mi = mapHeaders.find(hashBlock);
if (mi == mapHeaders.end())
return;
mapHeaders.erase(mi);
if (hashBestHeader == hashBlock)
RecomputeBestHeader();
}
void CSyncManager::ContinueHeaders(CNode* pfrom, const uint256& hashTip)
{
if (!pfrom || hashTip == 0)
return;
std::vector<uint256> vHave;
uint256 hashWalk = hashTip;
int nStep = 1;
while (hashWalk != 0)
{
vHave.push_back(hashWalk);
for (int i = 0; i < nStep && hashWalk != 0; ++i)
{
uint256 hashPrev = 0;
if (!GetPrevHash(hashWalk, hashPrev))
hashWalk = 0;
else
hashWalk = hashPrev;
}
if (vHave.size() > 10)
nStep *= 2;
}
vHave.push_back(!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet);
pfrom->PushMessage("getheaders", CBlockLocator(vHave), uint256(0));
}
bool CSyncManager::RequestRefill(CNode* pfrom, uint256 hashTip, int64_t nMinIntervalSeconds, const char* pszReason)
{
if (!pfrom || pfrom->fClient || pfrom->nVersion == 0 || !IsInitialBlockDownload())
return false;
const int64_t nNowSec = GetTime();
if (nMinIntervalSeconds > 0 &&
nNowSec - pfrom->nLastIbdHeaderRequest < nMinIntervalSeconds)
return false;
uint256 hashLocatorTip = hashTip;
if (hashLocatorTip == 0 ||
(!mapBlockIndex.count(hashLocatorTip) && !mapHeaders.count(hashLocatorTip)))
{
hashLocatorTip = hashBestHeader;
}
if (hashLocatorTip != 0 && (!pindexBest || hashLocatorTip != pindexBest->GetBlockHash()))
{
ContinueHeaders(pfrom, hashLocatorTip);
}
else
{
if (!pindexBest)
return false;
pfrom->pindexLastGetHeadersBegin = NULL;
pfrom->PushGetHeaders(pindexBest, uint256(0));
hashLocatorTip = pindexBest->GetBlockHash();
}
pfrom->nLastIbdHeaderRequest = nNowSec;
printf("IBD-DIAG: %s getheaders to peer=%s locator=%s plannerDepth=%u inflight=%u\n",
pszReason, pfrom->addr.ToString().c_str(),
hashLocatorTip.ToString().substr(0,20).c_str(),
GetPlannerDepth(), CountInFlight());
return true;
}
unsigned int CSyncManager::RequestRefillAllPeers(uint256 hashTip, int64_t nMinIntervalSeconds, const char* pszReason)
{
std::vector<CNode*> vEligiblePeers;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
if (!pnode->fClient && pnode->nVersion != 0 && !pnode->fDisconnect)
vEligiblePeers.push_back(pnode);
}
}
unsigned int nRequested = 0;
for (CNode* pnode : vEligiblePeers)
{
if (RequestRefill(pnode, hashTip, nMinIntervalSeconds, pszReason))
++nRequested;
}
return nRequested;
}
unsigned int CSyncManager::QueueBlocksParallel(unsigned int nWindow)
{
if (hashBestHeader == 0)
return 0;
const std::vector<uint256> vPath = GetDownloadPath(hashBestHeader);
if (vPath.empty())
return 0;
std::vector<CNode*> vEligiblePeers;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
if (!pnode->fClient && pnode->nVersion != 0 && !pnode->fDisconnect)
vEligiblePeers.push_back(pnode);
}
}
if (vEligiblePeers.empty())
return 0;
const int64_t nNow = GetTime() * 1000000;
unsigned int nInFlight = CountInFlight();
unsigned int nQueued = 0;
unsigned int nPeerIndex = 0;
std::sort(vEligiblePeers.begin(), vEligiblePeers.end(),
[](const CNode* a, const CNode* b) {
return a->nBlocksDelivered > b->nBlocksDelivered;
});
std::vector<CNode*> vWeightedPeers;
for (size_t i = 0; i < vEligiblePeers.size(); i++)
{
int nWeight = (i == 0) ? 3 : (i == 1) ? 2 : 1;
for (int w = 0; w < nWeight; w++)
vWeightedPeers.push_back(vEligiblePeers[i]);
}
int64_t nAdaptiveTimeout = HEADER_REQUEST_TIMEOUT_MICROS;
{
int64_t nTotalLatency = 0;
int nPeersWithLatency = 0;
for (const CNode* pnode : vEligiblePeers)
{
if (pnode->nAvgBlockLatencyUs > 0)
{
nTotalLatency += pnode->nAvgBlockLatencyUs;
++nPeersWithLatency;
}
}
if (nPeersWithLatency > 0)
{
int64_t nAvgLatency = nTotalLatency / nPeersWithLatency;
nAdaptiveTimeout = std::max((int64_t)(10 * 1000000),
std::min((int64_t)(60 * 1000000), nAvgLatency * 5));
}
}
for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it)
{
if (nInFlight + nQueued >= nWindow)
break;
std::map<uint256, HeaderNode>::iterator mi = mapHeaders.find(*it);
if (mi == mapHeaders.end())
continue;
bool fNeedsRequest = false;
if (!mi->second.fRequested)
fNeedsRequest = true;
else if (nNow - mi->second.nLastRequestTime >= nAdaptiveTimeout)
fNeedsRequest = true;
else if (nNow - mi->second.nLastRequestTime >= HEADER_REDUNDANT_REQUEST_MICROS)
fNeedsRequest = true;
if (!fNeedsRequest)
continue;
CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
pnode->AskFor(CInv(MSG_BLOCK, *it));
if (IsInitialBlockDownload() &&
vWeightedPeers.size() >= 2 &&
vWeightedPeers.size() < HEADER_REDUNDANT_PEER_THRESHOLD &&
!mi->second.fRequested)
{
CNode* pnode2 = vWeightedPeers[(nPeerIndex + 1) % vWeightedPeers.size()];
if (pnode2 != pnode)
pnode2->AskFor(CInv(MSG_BLOCK, *it));
}
if (!mi->second.fRequested || nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
{
if (!mi->second.fRequested)
mi->second.nFirstRequestTime = nNow;
mi->second.fRequested = true;
mi->second.nLastRequestTime = nNow;
}
++nQueued;
++nPeerIndex;
}
if (nQueued > 0)
printf("IBD-DIAG: sync manager queued %u blocks across %zu peers (window=%u, inflight=%u)\n",
nQueued, vEligiblePeers.size(), nWindow, nInFlight);
return nQueued;
}
bool CSyncManager::ProcessHeaders(CNode* pfrom, const std::vector<CBlock>& vHeaders)
{
if (vHeaders.size() > 2000)
{
pfrom->Misbehaving(20);
return error("message headers size() = %" PRIszu "", vHeaders.size());
}
uint256 hashChainTip = 0;
int nNewHeaders = 0;
for (const CBlock& header : vHeaders)
{
if (!header.vtx.empty())
{
pfrom->Misbehaving(20);
return error("headers message includes transactions");
}
const uint256 hashHeader = header.GetHash();
if (mapBlockIndex.count(hashHeader) || mapHeaders.count(hashHeader))
{
hashChainTip = hashHeader;
continue;
}
if (hashChainTip != 0)
{
if (header.hashPrevBlock != hashChainTip)
{
pfrom->Misbehaving(20);
return error("non-continuous headers sequence");
}
}
else
{
std::map<uint256, CBlockIndex*>::iterator miPrev = mapBlockIndex.find(header.hashPrevBlock);
if (miPrev == mapBlockIndex.end() && !mapHeaders.count(header.hashPrevBlock))
break;
}
if (!AddHeaderNode(header, hashHeader))
{
pfrom->Misbehaving(20);
return error("invalid header sequence");
}
hashChainTip = hashHeader;
nNewHeaders++;
}
int nRequested = 0;
if (hashBestHeader != 0)
nRequested = QueueBlocksParallel(HEADER_DOWNLOAD_WINDOW);
if (nNewHeaders > 0)
nLastNewHeaderTime = GetTime();
if (nNewHeaders > 0 || nRequested > 0)
printf("IBD-DIAG: accepted %d new headers, queued %d blocks from %zu headers (peer=%s bestHeader=%s)\n",
nNewHeaders, nRequested, vHeaders.size(), pfrom->addr.ToString().c_str(),
hashBestHeader.ToString().substr(0,20).c_str());
if (vHeaders.size() >= 2000)
{
if (IsInitialBlockDownload() && hashChainTip != 0)
ContinueHeaders(pfrom, hashChainTip);
else
pfrom->PushGetBlocks(pindexBest, uint256(0));
}
else if (IsInitialBlockDownload() && nNewHeaders > 0 && hashChainTip != 0)
{
ContinueHeaders(pfrom, hashChainTip);
}
else if (IsInitialBlockDownload())
{
const unsigned int nPlannerDepth = GetPlannerDepth();
if (nPlannerDepth <= HEADER_SYNC_LOW_WATER)
RequestRefill(
pfrom, (hashChainTip != 0) ? hashChainTip : hashBestHeader,
HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS,
(nPlannerDepth == 0) ? "headers planner empty" : "headers planner low-water");
}
return true;
}
void CSyncManager::TrackBlockDelivery(CNode* pfrom, const uint256& hashBlock)
{
if (!pfrom)
return;
pfrom->nBlocksDelivered++;
if (nBestHeight > pfrom->nBestKnownHeight)
pfrom->nBestKnownHeight = nBestHeight;
int64_t nRequestTime = GetRequestTime(hashBlock);
if (nRequestTime > 0)
{
int64_t nLatency = GetTime() * 1000000 - nRequestTime;
if (nLatency > 0)
{
if (pfrom->nAvgBlockLatencyUs == 0)
pfrom->nAvgBlockLatencyUs = nLatency;
else
pfrom->nAvgBlockLatencyUs = (pfrom->nAvgBlockLatencyUs * 7 + nLatency) / 8;
}
}
}
void CSyncManager::Tick(CNode* pto, int nHighestInvWalk, const uint256& hashHighestInvWalk)
{
if (!pto || pto->fClient || pto->nVersion == 0 || !IsInitialBlockDownload())
return;
const int64_t nNowSec = GetTime();
const unsigned int nPlannerDepth = GetPlannerDepth();
const unsigned int nInFlight = CountInFlight();
static int64_t nLastHeaderPlannerControl = 0;
static int64_t nLastHeaderWatchdog = 0;
static int64_t nLastBlockPlannerControl = 0;
if (nLastNewHeaderTime == 0)
nLastNewHeaderTime = nNowSec;
if (nNowSec - nLastHeaderPlannerControl >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS &&
nPlannerDepth < HEADER_SYNC_LOW_WATER &&
nInFlight < HEADER_SYNC_TARGET_INFLIGHT)
{
const unsigned int nRefilled = RequestRefillAllPeers(
hashBestHeader, HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS,
"control-loop");
if (nRefilled > 0)
printf("IBD-DIAG: control-loop refill from %u peers (plannerDepth=%u inflight=%u target=%u)\n",
nRefilled, nPlannerDepth, nInFlight, HEADER_SYNC_TARGET_INFLIGHT);
nLastHeaderPlannerControl = nNowSec;
}
if (nNowSec - nLastHeaderWatchdog >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS &&
nNowSec - nLastNewHeaderTime >= HEADER_SYNC_WATCHDOG_SECONDS)
{
const unsigned int nRefilled = RequestRefillAllPeers(
hashBestHeader, HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS,
"headers-watchdog");
if (nRefilled > 0)
printf("IBD-DIAG: headers watchdog refill from %u peers after %llds without new headers (plannerDepth=%u inflight=%u)\n",
nRefilled,
(long long)(nNowSec - nLastNewHeaderTime),
nPlannerDepth,
nInFlight);
nLastHeaderWatchdog = nNowSec;
}
const int64_t nMinInterval = (mapHeaders.size() < HEADER_DOWNLOAD_WINDOW) ? 15 : 60;
if (nNowSec - pto->nLastIbdHeaderRequest >= nMinInterval)
RequestRefill(pto, hashBestHeader, nMinInterval, "heartbeat");
if (nNowSec - nLastBlockPlannerControl >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS &&
hashBestHeader != 0 &&
nPlannerDepth > 0)
{
const unsigned int nRequeued = QueueBlocksParallel(HEADER_DOWNLOAD_WINDOW);
if (nRequeued > 0)
printf("IBD-DIAG: block-planner control queued %u block requests (plannerDepth=%u inflight=%u)\n",
nRequeued, nPlannerDepth, nInFlight);
nLastBlockPlannerControl = nNowSec;
}
if (hashBestHeader == 0 && nHighestInvWalk > nBestHeight &&
hashHighestInvWalk != 0 && mapBlockIndex.count(hashHighestInvWalk))
{
RequestRefill(pto, hashHighestInvWalk, HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS, "inv-walk bridge");
}
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_SYNCMANAGER_H
#define TRIANGLES_SYNCMANAGER_H
#include "uint256.h"
#include <cstddef>
#include <cstdint>
#include <vector>
class CBlock;
class CInv;
class CNode;
class CSyncManager
{
public:
struct HeaderNode;
static constexpr unsigned int HEADER_DOWNLOAD_WINDOW = 1024;
static constexpr unsigned int HEADER_SYNC_LOW_WATER = HEADER_DOWNLOAD_WINDOW / 4;
static constexpr unsigned int HEADER_SYNC_TARGET_INFLIGHT = HEADER_DOWNLOAD_WINDOW / 2;
static constexpr int64_t HEADER_SYNC_REFILL_MIN_INTERVAL_SECONDS = 5;
static constexpr int64_t HEADER_SYNC_CONTROL_INTERVAL_SECONDS = 5;
static constexpr int64_t HEADER_SYNC_WATCHDOG_SECONDS = 25;
bool HaveHeader(const uint256& hash) const;
uint256 GetBestHeader() const;
std::size_t GetHeaderCount() const;
unsigned int CountInFlight() const;
unsigned int GetPlannerDepth() const;
int GetPlannerHeight() const;
int64_t GetRequestTime(const uint256& hashBlock) const;
bool RequestRefill(CNode* pfrom, uint256 hashTip, int64_t nMinIntervalSeconds, const char* pszReason);
unsigned int RequestRefillAllPeers(uint256 hashTip, int64_t nMinIntervalSeconds, const char* pszReason);
unsigned int QueueBlocksParallel(unsigned int nWindow = HEADER_DOWNLOAD_WINDOW);
bool ProcessHeaders(CNode* pfrom, const std::vector<CBlock>& vHeaders);
void BlockAccepted(const uint256& hashBlock);
void TrackBlockDelivery(CNode* pfrom, const uint256& hashBlock);
void Tick(CNode* pto, int nHighestInvWalk, const uint256& hashHighestInvWalk);
private:
uint256 GetHeaderTrust(unsigned int nBits) const;
bool GetKnownHeaderState(const uint256& hash, int& nHeight, uint256& nChainTrust) const;
bool GetPrevHash(const uint256& hash, uint256& hashPrev) const;
void RecomputeBestHeader();
void PruneHeaders();
bool AddHeaderNode(const CBlock& header, const uint256& hashHeader);
std::vector<uint256> GetDownloadPath(uint256 hashTip) const;
void ContinueHeaders(CNode* pfrom, const uint256& hashTip);
};
extern CSyncManager g_syncManager;
#endif // TRIANGLES_SYNCMANAGER_H
+50
View File
@@ -16,6 +16,8 @@
#include <thread>
#include <fstream>
#include <cstring>
#include <chrono>
#include <ctime>
#include <vector>
#include <string>
@@ -122,11 +124,59 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
// Prepare Tor data directory under the wallet's data dir
torDataDir = (::GetDataDir() / "tor_data").string();
fs::create_directories(torDataDir);
// CRITICAL: Tor refuses to use a DataDirectory readable by other users.
// Without 0700, tor_run_main() returns -1 and the embedded Tor never starts.
fs::permissions(torDataDir, fs::perms::owner_all, fs::perm_options::replace);
// triangles fix: auto-repair `state`-as-file corruption (pitfall #19).
// Tor's atomic state-write pattern is: write `state.tmp` → rename to `state`.
// If the daemon is killed or the process crashes mid-write, the rename can
// fail and `state` may be left as a regular file (or a partial file). On
// next start, Tor sees "State file ... is not a file? Failing." and dies
// with code -1 ("Reading config failed"). This was hit on DNS3 on
// 2026-05-24 and on the TRI-LAPTOP GUI wallet on 2026-06-15. The user-facing
// symptom is "Tor failed to start. Triangles requires Tor to operate." and
// the only fix was manually renaming the corrupt file. Detect this state
// here and auto-rename so the daemon is self-healing.
{
fs::path statePath = fs::path(torDataDir) / "state";
std::error_code ec;
if (fs::exists(statePath, ec) && !fs::is_directory(statePath, ec)) {
// state is a file (or symlink to one) — quarantine it
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
char ts[32];
std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", std::gmtime(&t));
fs::path quarantine = fs::path(torDataDir) /
(std::string("state.corrupt-") + ts);
try {
fs::rename(statePath, quarantine, ec);
if (ec) {
// rename can fail on Windows if dest exists; remove then rename
fs::remove(quarantine, ec);
fs::rename(statePath, quarantine, ec);
}
printf("Tor state was a file (corrupt) — quarantined to %s for inspection. Tor will recreate state/ as a directory.\n",
quarantine.filename().string().c_str());
} catch (const std::exception& e) {
printf("WARNING: could not quarantine corrupt Tor state file %s: %s\n",
statePath.string().c_str(), e.what());
// Last resort: try to remove it so Tor can proceed
fs::remove(statePath, ec);
}
}
}
std::string hsDir;
if (hiddenServiceEnabled) {
hsDir = (fs::path(torDataDir) / "hidden_service").string();
fs::create_directories(hsDir);
// CRITICAL: Tor rejects hidden service directories that are not 0700
// ("Permissions on directory ... are too permissive") and aborts config
// validation with code -1. This was the root cause of "Embedded Tor
// exited with code -1" — fs::create_directories honors umask (0022 on
// most Linux systems), leaving the dir at 0755. Force 0700 after creation.
fs::permissions(hsDir, fs::perms::owner_all, fs::perm_options::replace);
}
// Build the argv for tor_run_main
+28 -4
View File
@@ -259,10 +259,34 @@ bool CTorProcess::WriteTorrc()
// SOCKS proxy for wallet connections
torrc << "SocksPort " << socksPort << "\n";
// Data directory for Tor state
fs::path torStateDir = dataPath / "state";
fs::create_directories(torStateDir);
torrc << "DataDirectory " << torStateDir.string() << "\n";
// Data directory for Tor state.
// Use the tor_data directory itself as DataDirectory so that Tor creates
// its internal 'state' FILE at <tor_data>/state. Older wallet builds
// erroneously created a subdirectory called 'state' and pointed
// DataDirectory at it; newer Tor versions (0.4.9+) reject that because
// they expect to write a plain file called 'state' inside DataDirectory.
//
// Recovery: if 'state' exists as a directory, move its contents up and
// remove it so that Tor can create its state file in the normal location.
{
fs::path badStateDir = dataPath / "state";
if (fs::exists(badStateDir) && fs::is_directory(badStateDir)) {
// Migrate any files inside the bad 'state/' directory up to dataPath
try {
for (auto& entry : fs::directory_iterator(badStateDir)) {
fs::path dest = dataPath / entry.path().filename();
if (!fs::exists(dest)) {
fs::rename(entry.path(), dest);
}
}
fs::remove(badStateDir);
printf("Auto-recovered: removed legacy 'state' directory from %s\n", dataPath.string().c_str());
} catch (const fs::filesystem_error& e) {
printf("WARNING: Could not auto-recover tor_data/state directory: %s\n", e.what());
}
}
}
torrc << "DataDirectory " << dataPath.string() << "\n";
// Persistent Tor log for post-mortem debugging on user machines.
fs::path torLogPath = dataPath / "tor.log";
+616
View File
@@ -0,0 +1,616 @@
// Copyright (c) 2014-2026 The Cryptographic Triangles developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// triangles-cli — JSON-RPC client for trianglesd.
//
// Talks to a running trianglesd over HTTP/1.1 with HTTP Basic auth and
// JSON-RPC 1.0. Patterned after bitcoin-cli (Bitcoin Core) and dash-cli.
//
// Build with -DBUILD_CLI=ON (default ON).
//
// Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common.
// Only links json_compat (nlohmann/json via json_spirit shim) and the platform's
// native socket library (Winsock on Windows, libc on POSIX). No Boost dependency
// at all — keeps the binary small and avoids platform-specific link problems
// with boost::asio / libboost_system (MSYS2 names them with -mt- versioned
// suffixes; Homebrew doesn't ship the CMake config for the system component).
//
// Connection parameters (highest precedence first):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
// 2. triangles.conf in the data directory (or -conf=<path>)
// 3. Defaults: 127.0.0.1:19111 mainnet, 19112 testnet; no auth (must be set in conf)
//
// Usage:
// triangles-cli help List commands (delegates to daemon)
// triangles-cli help <command> Help for one command
// triangles-cli getinfo Example: summary info
// triangles-cli getblockchaininfo Example: chain state
// triangles-cli getbalance Example: 0-arg call
// triangles-cli getbalance "*" 6 Example: positional args
// triangles-cli sendtoaddress <addr> 1.5 "memo" Example: mixed types
// triangles-cli -getinfo Synthesized summary from multiple RPCs
// triangles-cli -raw <method> <args...> Print raw JSON response (no pretty-print)
//
// Any command-line arg that parses as a JSON literal (number, bool, null,
// object, array) is forwarded as that literal; otherwise it is sent as a JSON
// string. This matches bitcoin-cli semantics.
#define TRIANGLES_CLI_VERSION "1.0.0"
#include "json/json_compat.h"
#include <filesystem>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
// Cross-platform socket includes
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
using socket_t = SOCKET;
#define TRI_CLI_INVALID_SOCKET INVALID_SOCKET
#define TRI_CLI_CLOSE_SOCKET(s) closesocket(s)
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
using socket_t = int;
#define TRI_CLI_INVALID_SOCKET (-1)
#define TRI_CLI_CLOSE_SOCKET(s) close(s)
#endif
using namespace std;
namespace fs = std::filesystem;
using namespace json_spirit;
// ─────────────────────────────────────────────────────────────────────────────
// Minimal arg/config plumbing — self-contained, no util.cpp dep.
// ─────────────────────────────────────────────────────────────────────────────
static map<string, string> mapArgs;
static map<string, vector<string> > mapMultiArgs;
static string GetArg(const string& key, const string& def = "")
{
auto it = mapArgs.find(key);
return (it != mapArgs.end()) ? it->second : def;
}
static bool GetBoolArg(const string& key, bool def)
{
auto it = mapArgs.find(key);
if (it == mapArgs.end()) return def;
string v = it->second;
if (v.empty()) return true;
return (v != "0" && v != "false" && v != "no");
}
static void ReadConfigFile(const string& path)
{
ifstream f(path);
if (!f.good()) return;
string line;
while (getline(f, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t start = line.find_first_not_of(" \t");
if (start == string::npos) continue;
if (line[start] == '#') continue;
size_t eq = line.find('=', start);
if (eq == string::npos) continue;
string key = line.substr(start, eq - start);
string value = line.substr(eq + 1);
auto trim = [](string& s) {
size_t a = s.find_first_not_of(" \t");
size_t b = s.find_last_not_of(" \t");
if (a == string::npos) { s.clear(); return; }
s = s.substr(a, b - a + 1);
};
trim(key);
trim(value);
if (value.size() >= 2 &&
((value.front() == '"' && value.back() == '"') ||
(value.front() == '\'' && value.back() == '\''))) {
value = value.substr(1, value.size() - 2);
}
string dashKey = "-" + key;
if (mapArgs.count(dashKey) == 0) {
mapArgs[dashKey] = value;
mapMultiArgs[dashKey].push_back(value);
}
}
}
static fs::path GetDefaultDataDir()
{
#ifdef _WIN32
const char* appdata = getenv("APPDATA");
if (appdata && *appdata) {
return fs::path(appdata) / "CryptographicTriangles";
}
return fs::path("C:/CryptographicTriangles");
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (home && *home) {
return fs::path(home) / "Library/Application Support/CryptographicTriangles";
}
return fs::path("/tmp/CryptographicTriangles");
#else
const char* home = getenv("HOME");
if (home && *home) {
return fs::path(home) / ".cryptographic-triangles";
}
return fs::path("/tmp/CryptographicTriangles");
#endif
}
static fs::path GetConfigFilePath()
{
fs::path confPath = GetArg("-conf", "triangles.conf");
if (confPath.is_absolute()) return confPath;
fs::path datadir = GetArg("-datadir", "");
if (datadir.empty()) datadir = GetDefaultDataDir().string();
return fs::path(datadir) / confPath;
}
// ─────────────────────────────────────────────────────────────────────────────
// Command-line parsing
// ─────────────────────────────────────────────────────────────────────────────
static void ParseCommandLine(int argc, char* const argv[])
{
mapArgs.clear();
mapMultiArgs.clear();
for (int i = 1; i < argc; ++i) {
string str(argv[i]);
if (str == "-") {
mapMultiArgs["-"].push_back("-");
continue;
}
string strKey, strVal;
size_t idx = str.find('=');
if (idx == string::npos) {
strKey = "-" + str;
strVal = "1";
} else {
strKey = "-" + str.substr(0, idx);
strVal = str.substr(idx + 1);
}
mapArgs[strKey] = strVal;
mapMultiArgs[strKey].push_back(strVal);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// RPC connection parameters
// ─────────────────────────────────────────────────────────────────────────────
struct RPCConn {
string host = "127.0.0.1";
string port = "19111";
string user;
string pass;
};
static int AppInitRPCConn(RPCConn& conn)
{
fs::path confPath = GetConfigFilePath();
if (!confPath.empty()) ReadConfigFile(confPath.string());
bool fTestNet = GetBoolArg("-testnet", false);
conn.port = GetArg("-rpcport", fTestNet ? "19112" : "19111");
conn.host = GetArg("-rpcconnect", "127.0.0.1");
conn.user = GetArg("-rpcuser", "");
conn.pass = GetArg("-rpcpassword", "");
if (conn.user.empty() || conn.pass.empty()) {
cerr << "triangles-cli: missing RPC credentials. Set rpcuser/rpcpassword in triangles.conf\n"
<< " or pass -rpcuser=<user> -rpcpassword=<pw> on the command line.\n"
<< " (RPC config file: " << confPath.string() << ")\n";
return 1;
}
return 0;
}
// ─────────────────────────────────────────────────────────────────────────────
// JSON-RPC param conversion
// ─────────────────────────────────────────────────────────────────────────────
static Value ParseCLIParam(const string& arg)
{
if (arg.empty()) {
return Value(string(""));
}
Value v;
if (read_string(arg, v) && v.type() != str_type) {
return v;
}
return Value(arg);
}
static void ParseCommandLineRPCParams(int argc, char* const argv[],
Value& method, Array& params)
{
method = Value(string(""));
params.clear();
int i = 1;
static const set<string> valFlags = {
"-conf", "-datadir", "-rpcconnect", "-rpcport",
"-rpcuser", "-rpcpassword"
};
while (i < argc) {
string arg(argv[i]);
if (arg == "-" || arg.size() < 2 || arg[0] != '-') break;
if (valFlags.count(arg) && i + 1 < argc &&
string(argv[i+1]).substr(0,1) != "-") {
i += 2;
} else {
++i;
}
}
if (i >= argc) {
method = Value(string("help"));
return;
}
method = Value(string(argv[i]));
++i;
while (i < argc) {
string arg(argv[i]);
if (arg == "-") {
string line;
while (getline(cin, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
params.push_back(ParseCLIParam(line));
}
} else {
params.push_back(ParseCLIParam(arg));
}
++i;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Base64 (RFC 4648) — for HTTP Basic auth
// ─────────────────────────────────────────────────────────────────────────────
static const char b64_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static string Base64Encode(const string& in)
{
string out;
out.reserve(((in.size() + 2) / 3) * 4);
int val = 0, valb = -6;
for (unsigned char c : in) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(b64_table[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(b64_table[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
// ─────────────────────────────────────────────────────────────────────────────
// HTTP/1.1 JSON-RPC POST (plaintext) — using raw sockets (no Boost)
// ─────────────────────────────────────────────────────────────────────────────
namespace {
class SocketInit {
public:
SocketInit() {
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
}
~SocketInit() {
#ifdef _WIN32
WSACleanup();
#endif
}
};
inline void close_socket(socket_t s) {
TRI_CLI_CLOSE_SOCKET(s);
}
} // namespace
static int CallRPC(const RPCConn& conn, const string& strMethod,
const Array& params, Value& result)
{
SocketInit sockInit;
Object req;
req.push_back(Pair("jsonrpc", Value(string("1.0"))));
req.push_back(Pair("id", Value(string("triangles-cli"))));
req.push_back(Pair("method", Value(strMethod)));
req.push_back(Pair("params", Value(params)));
string strRequest = write_string(Value(req), false) + "\n";
string strAuth = Base64Encode(conn.user + ":" + conn.pass);
// Resolve host:port via getaddrinfo
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo* addrRes = nullptr;
int rc = getaddrinfo(conn.host.c_str(), conn.port.c_str(), &hints, &addrRes);
if (rc != 0 || addrRes == nullptr) {
cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port
<< " failed: " << gai_strerror(rc) << "\n";
if (addrRes) freeaddrinfo(addrRes);
return 1;
}
// Try each resolved address until one connects
socket_t sock = TRI_CLI_INVALID_SOCKET;
for (struct addrinfo* ai = addrRes; ai != nullptr; ai = ai->ai_next) {
sock = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (sock == TRI_CLI_INVALID_SOCKET) {
continue;
}
if (::connect(sock, ai->ai_addr, ai->ai_addrlen) == 0) {
break; // connected
}
close_socket(sock);
sock = TRI_CLI_INVALID_SOCKET;
}
freeaddrinfo(addrRes);
if (sock == TRI_CLI_INVALID_SOCKET) {
cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port
<< " failed\n"
<< "(is trianglesd running and accepting JSON-RPC?)\n";
return 1;
}
// Build HTTP/1.1 request
string reqData =
"POST / HTTP/1.1\r\n"
"Host: " + conn.host + ":" + conn.port + "\r\n"
"Authorization: Basic " + strAuth + "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " + to_string(strRequest.size()) + "\r\n"
"Connection: close\r\n"
"\r\n" + strRequest;
// Send
size_t totalSent = 0;
while (totalSent < reqData.size()) {
ssize_t n = ::send(sock, reqData.data() + totalSent,
reqData.size() - totalSent, 0);
if (n <= 0) {
cerr << "triangles-cli: write failed\n";
close_socket(sock);
return 1;
}
totalSent += static_cast<size_t>(n);
}
// Read full response (until EOF)
string respData;
char buf[4096];
while (true) {
ssize_t n = ::recv(sock, buf, sizeof(buf), 0);
if (n > 0) {
respData.append(buf, static_cast<size_t>(n));
} else if (n == 0) {
break; // EOF
} else {
// Error
#ifdef _WIN32
int err = WSAGetLastError();
if (err == WSAECONNRESET || err == WSAECONNABORTED) {
// Treat as EOF
break;
}
#else
if (errno == EINTR) continue; // interrupted, retry
if (errno == ECONNRESET) break; // peer closed
#endif
cerr << "triangles-cli: read failed\n";
close_socket(sock);
return 1;
}
}
close_socket(sock);
// Parse status line
size_t hdrEnd = respData.find("\r\n\r\n");
if (hdrEnd == string::npos) {
cerr << "triangles-cli: malformed response (no header terminator)\n";
return 1;
}
string statusLine = respData.substr(0, respData.find("\r\n"));
int status = 0;
{
istringstream iss(statusLine);
string httpVer;
iss >> httpVer >> status;
}
if (status != 200) {
cerr << "triangles-cli: server returned HTTP " << status << "\n";
string body = respData.substr(hdrEnd + 4);
if (!body.empty()) cerr << body << "\n";
return 1;
}
string body = respData.substr(hdrEnd + 4);
Value reply;
if (!read_string(body, reply)) {
cerr << "triangles-cli: could not parse JSON response:\n" << body << "\n";
return 1;
}
if (reply.type() != obj_type) {
cerr << "triangles-cli: unexpected response (not an object):\n"
<< write_string(reply, true) << "\n";
return 1;
}
Object replyObj = reply.get_obj();
const Value& err = find_value(replyObj, "error");
if (err.type() != null_type) {
cerr << "RPC error: " << write_string(err, false) << "\n";
return 1;
}
const Value& res = find_value(replyObj, "result");
result = res;
return 0;
}
// ─────────────────────────────────────────────────────────────────────────────
// triangles-cli -getinfo — synthesize a friendly summary from a few RPC calls
// ─────────────────────────────────────────────────────────────────────────────
static int Getinfo(const RPCConn& conn, bool fPretty)
{
Object info;
Value r;
Array emptyParams;
if (CallRPC(conn, "getnetworkinfo", emptyParams, r) == 0) {
info.push_back(Pair("network", r));
}
if (CallRPC(conn, "getblockchaininfo", emptyParams, r) == 0) {
Object chain = r.get_obj();
info.push_back(Pair("blockchain", r));
info.push_back(Pair("blocks", find_value(chain, "blocks")));
info.push_back(Pair("headers", find_value(chain, "headers")));
info.push_back(Pair("bestblockhash", find_value(chain, "bestblockhash")));
info.push_back(Pair("difficulty", find_value(chain, "difficulty")));
info.push_back(Pair("verificationprogress",
find_value(chain, "verificationprogress")));
info.push_back(Pair("chain", find_value(chain, "chain")));
}
if (CallRPC(conn, "getwalletinfo", emptyParams, r) == 0) {
Object wal = r.get_obj();
info.push_back(Pair("wallet", r));
info.push_back(Pair("balance", find_value(wal, "balance")));
}
Object connObj;
connObj.push_back(Pair("rpcconnect", Value(conn.host)));
connObj.push_back(Pair("rpcport", Value(conn.port)));
info.push_back(Pair("connection", Value(connObj)));
cout << write_string(Value(info), fPretty) << "\n";
return 0;
}
// ─────────────────────────────────────────────────────────────────────────────
// Help / version
// ─────────────────────────────────────────────────────────────────────────────
static int CommandLineHelp(ostream& out)
{
out << "Usage: triangles-cli [options] <command> [params]\n"
<< "\n"
<< " triangles-cli [options] help List commands (delegates to daemon)\n"
<< " triangles-cli [options] help <command> Help for one command (delegates to daemon)\n"
<< " triangles-cli -getinfo Show summary info from the daemon\n"
<< "\n"
<< "Options:\n"
<< " -conf=<file> Specify configuration file (default: triangles.conf)\n"
<< " -datadir=<dir> Specify data directory\n"
<< " -testnet Use testnet (RPC port 19112)\n"
<< " -rpcconnect=<ip> Send commands to node running on <ip> (default: 127.0.0.1)\n"
<< " -rpcport=<port> Connect to JSON-RPC on <port> (default: 19111 or testnet: 19112)\n"
<< " -rpcuser=<user> Username for JSON-RPC connections\n"
<< " -rpcpassword=<pw> Password for JSON-RPC connections\n"
<< " -stdin Read extra params from standard input, one per line\n"
<< " -raw Print raw JSON response (no pretty-printing)\n"
<< " -version Print version and exit\n"
<< "\n"
<< "Examples:\n"
<< " triangles-cli getinfo\n"
<< " triangles-cli getblockchaininfo\n"
<< " triangles-cli getbalance\n"
<< " triangles-cli getbalance \"*\" 6\n"
<< " triangles-cli sendtoaddress <address> <amount> [comment]\n"
<< " triangles-cli -getinfo\n"
<< "\n";
return 0;
}
static int CommandLineVersion()
{
cout << "triangles-cli version " << TRIANGLES_CLI_VERSION
<< " (Cryptographic Triangles RPC client)\n";
return 0;
}
// ─────────────────────────────────────────────────────────────────────────────
// main
// ─────────────────────────────────────────────────────────────────────────────
int main(int argc, char* argv[])
{
ParseCommandLine(argc, argv);
if (argc < 2 || GetArg("-?", "") == "1" || GetArg("-h", "") == "1" ||
GetArg("--help", "") == "1") {
CommandLineHelp(cerr);
return argc < 2 ? 1 : 0;
}
if (!GetArg("-version", "").empty() || !GetArg("--version", "").empty()) {
CommandLineVersion();
return 0;
}
RPCConn conn;
if (AppInitRPCConn(conn) != 0) return 1;
Value method;
Array params;
ParseCommandLineRPCParams(argc, argv, method, params);
string strMethod = method.get_str();
if (strMethod == "help" || strMethod == "-help") {
if (params.empty()) {
CommandLineHelp(cout);
return 0;
}
}
if (!GetArg("-getinfo", "").empty()) {
return Getinfo(conn, /*fPretty=*/true);
}
bool fPretty = GetArg("-raw", "").empty();
Value result;
int nRet = CallRPC(conn, strMethod, params, result);
if (nRet == 0) {
cout << write_string(result, fPretty) << "\n";
}
return nRet;
}
+70 -34
View File
@@ -305,6 +305,10 @@ static const CRPCCommand vRPCCommands[] =
{ "settxfee", &settxfee, false, false },
{ "listsinceblock", &listsinceblock, false, false },
{ "dumpprivkey", &dumpprivkey, false, false },
{ "hdnew", &hdnew, false, false },
{ "hdrestore", &hdrestore, false, false },
{ "hdshow", &hdshow, false, false },
{ "hdinfo", &hdinfo, true, false },
{ "dumpwallet", &dumpwallet, true, false },
{ "importwallet", &importwallet, false, false },
{ "importprivkey", &importprivkey, false, false },
@@ -546,10 +550,17 @@ int ReadHTTP(std::basic_istream<char>& stream, map<string, string>& mapHeadersRe
bool HTTPAuthorized(map<string, string>& mapHeaders)
{
string strAuth = mapHeaders["authorization"];
if (strAuth.substr(0,6) != "Basic ")
if (strAuth.size() < 6 || strAuth.substr(0,6) != "Basic ")
return false;
string strUserPass64 = strAuth.substr(6); strUserPass64 = TrimString(strUserPass64);
string strUserPass = DecodeBase64(strUserPass64);
if (strUserPass64.empty())
return false;
string strUserPass;
try {
strUserPass = DecodeBase64(strUserPass64);
} catch (const std::exception&) {
return false;
}
return TimingResistantEqual(strUserPass, strRPCUserColonPass);
}
@@ -783,44 +794,61 @@ static void RPCAcceptHandler(boost::shared_ptr< basic_socket_acceptor<Protocol,
{
vnThreadsRunning[THREAD_RPCLISTENER]++;
// Immediately start accepting new connections, except when we're cancelled or our socket is closed.
if (error != asio::error::operation_aborted
&& acceptor->is_open())
RPCListen(acceptor, context, fUseSSL);
try {
// Immediately start accepting new connections, except when we're cancelled or our socket is closed.
if (error != asio::error::operation_aborted
&& acceptor->is_open())
RPCListen(acceptor, context, fUseSSL);
AcceptedConnectionImpl<ip::tcp>* tcp_conn = dynamic_cast< AcceptedConnectionImpl<ip::tcp>* >(conn);
AcceptedConnectionImpl<ip::tcp>* tcp_conn = dynamic_cast< AcceptedConnectionImpl<ip::tcp>* >(conn);
if (error)
{
if (error != asio::error::operation_aborted)
printf("RPC accept error from %s: %s (%d)\n",
tcp_conn ? tcp_conn->peer.address().to_string().c_str() : "unknown peer",
error.message().c_str(),
error.value());
if (error)
{
if (error != asio::error::operation_aborted)
printf("RPC accept error from %s: %s (%d)\n",
tcp_conn ? tcp_conn->peer.address().to_string().c_str() : "unknown peer",
error.message().c_str(),
error.value());
delete conn;
vnThreadsRunning[THREAD_RPCLISTENER]--;
return;
}
// Restrict callers by IP. It is important to
// do this before starting client thread, to filter out
// certain DoS and misbehaving clients.
else if (tcp_conn
&& !ClientAllowed(tcp_conn->peer.address()))
{
// Only send a 403 if we're not using SSL to prevent a DoS during the SSL handshake.
try {
if (!fUseSSL)
conn->stream() << HTTPReply(HTTP_FORBIDDEN, "", false) << std::flush;
} catch (const std::exception& e) {
printf("RPC error sending 403 to %s: %s\n",
tcp_conn->peer.address().to_string().c_str(), e.what());
}
delete conn;
vnThreadsRunning[THREAD_RPCLISTENER]--;
return;
}
// start HTTP client thread
else if (!NewThread(ThreadRPCServer3, conn)) {
printf("Failed to create RPC server client thread\n");
delete conn;
}
vnThreadsRunning[THREAD_RPCLISTENER]--;
} catch (std::exception& e) {
PrintException(&e, "RPCAcceptHandler()");
delete conn;
vnThreadsRunning[THREAD_RPCLISTENER]--;
return;
}
// Restrict callers by IP. It is important to
// do this before starting client thread, to filter out
// certain DoS and misbehaving clients.
else if (tcp_conn
&& !ClientAllowed(tcp_conn->peer.address()))
{
// Only send a 403 if we're not using SSL to prevent a DoS during the SSL handshake.
if (!fUseSSL)
conn->stream() << HTTPReply(HTTP_FORBIDDEN, "", false) << std::flush;
} catch (...) {
PrintException(NULL, "RPCAcceptHandler()");
delete conn;
vnThreadsRunning[THREAD_RPCLISTENER]--;
}
// start HTTP client thread
else if (!NewThread(ThreadRPCServer3, conn)) {
printf("Failed to create RPC server client thread\n");
delete conn;
}
vnThreadsRunning[THREAD_RPCLISTENER]--;
}
void ThreadRPCServer2(void* parg)
@@ -1128,6 +1156,7 @@ void ThreadRPCServer3(void* parg)
AcceptedConnection *conn = (AcceptedConnection *) parg;
bool fRun = true;
try {
while (true)
{
if (fShutdown || !fRun)
@@ -1247,6 +1276,13 @@ void ThreadRPCServer3(void* parg)
}
}
} // end try
catch (std::exception& e) {
PrintException(&e, "ThreadRPCServer3()");
} catch (...) {
PrintException(NULL, "ThreadRPCServer3()");
}
delete conn;
{
LOCK(cs_THREAD_RPCHANDLER);
+4
View File
@@ -155,6 +155,10 @@ extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool f
extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value dumpprivkey(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp
extern json_spirit::Value hdnew(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdrestore(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdshow(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importprivkey(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getsubsidy(const json_spirit::Array& params, bool fHelp);
+25
View File
@@ -514,6 +514,20 @@ bool CTxDB::LoadBlockIndex()
nBestHeight = pindexBest->nHeight;
nBestChainTrust = pindexBest->nChainTrust;
// Heal pnext pointers along the active chain. Persisted hashNext can be
// stale or zeroed by crash-interrupted reorgs, which breaks
// GetKernelStakeModifier()'s forward walk and causes valid new
// proof-of-stake blocks to be rejected with "check kernel failed".
{
int nHealed = 0;
for (CBlockIndex* p = pindexBest; p && p->pprev; p = p->pprev)
{
if (p->pprev->pnext != p) { p->pprev->pnext = p; nHealed++; }
}
if (nHealed > 0)
printf("LoadBlockIndex(): healed %d pnext links on active chain\n", nHealed);
}
printf("STARTUP-PERF: best_chain %" PRId64 "ms\n", GetTimeMillis() - nPhaseStart);
nPhaseStart = GetTimeMillis();
@@ -610,7 +624,18 @@ bool CTxDB::LoadBlockIndex()
break;
CBlock block;
if (!block.ReadFromDisk(pindex))
{
// Snapshot-sourced chains have block headers + UTXOs but not raw
// block bodies on disk yet. Skip verification for those — the
// UTXO set itself was content-hash verified during LoadSnapshot.
// For non-snapshot chains, this remains a fatal error.
if (fLoadedFromSnapshot) {
printf("LoadBlockIndex(): block %d not on disk (snapshot-sourced), skipping verification\n",
pindex->nHeight);
continue;
}
return error("LoadBlockIndex() : block.ReadFromDisk failed");
}
if (nCheckLevel>0 && !block.CheckBlock(true, true, (nCheckLevel>6)))
{
printf("LoadBlockIndex() : *** found bad block at %d, hash=%s\n", pindex->nHeight, pindex->GetBlockHash().ToString().c_str());
+2 -1
View File
@@ -42,12 +42,13 @@ public:
bool LoadBlockIndex() override;
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
protected:
bool ReadRaw(const std::string& key, std::string& value) const override;
bool WriteRaw(const std::string& key, const std::string& value) override;
bool EraseRaw(const std::string& key) override;
bool ExistsRaw(const std::string& key) const override;
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
private:
leveldb::DB* pdb; // Points to the global instance.
+21
View File
@@ -542,6 +542,20 @@ bool CRocksTxDB::LoadBlockIndex()
nBestHeight = pindexBest->nHeight;
nBestChainTrust = pindexBest->nChainTrust;
// Heal pnext pointers along the active chain. Persisted hashNext can be
// stale or zeroed by crash-interrupted reorgs, which breaks
// GetKernelStakeModifier()'s forward walk and causes valid new
// proof-of-stake blocks to be rejected with "check kernel failed".
{
int nHealed = 0;
for (CBlockIndex* p = pindexBest; p && p->pprev; p = p->pprev)
{
if (p->pprev->pnext != p) { p->pprev->pnext = p; nHealed++; }
}
if (nHealed > 0)
printf("LoadBlockIndex(): healed %d pnext links on active chain\n", nHealed);
}
printf("STARTUP-PERF: best_chain %" PRId64 "ms\n", GetTimeMillis() - nPhaseStart);
nPhaseStart = GetTimeMillis();
@@ -640,7 +654,14 @@ bool CRocksTxDB::LoadBlockIndex()
break;
CBlock block;
if (!block.ReadFromDisk(pindex))
{
if (fLoadedFromSnapshot) {
printf("LoadBlockIndex(): block %d not on disk (snapshot-sourced), skipping verification\n",
pindex->nHeight);
continue;
}
return error("LoadBlockIndex(): block.ReadFromDisk failed");
}
if (nCheckLevel > 0 && !block.CheckBlock(true, true, (nCheckLevel > 6)))
{
printf("LoadBlockIndex(): bad block at %d, hash=%s\n",
+11 -1
View File
@@ -38,12 +38,22 @@ public:
bool LoadBlockIndex() override;
// Write a raw serialized key/value pair, bypassing the typed Write<>()
// overloads. Intended for the chaindb migration utility, which carries
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
// active write batch if one is open.
bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
{
return WriteRaw(key, value);
}
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
protected:
bool ReadRaw(const std::string& key, std::string& value) const override;
bool WriteRaw(const std::string& key, const std::string& value) override;
bool EraseRaw(const std::string& key) override;
bool ExistsRaw(const std::string& key) const override;
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
private:
rocksdb::DB* pdb; // Points to the global instance.
+31
View File
@@ -600,6 +600,37 @@ bool SoftSetBoolArg(const std::string& strArg, bool fValue)
return SoftSetArg(strArg, std::string("0"));
}
// C++20 modernization: std::string_view overloads delegating to std::string implementations
std::string GetArg(std::string_view strArg, std::string_view strDefault)
{
return GetArg(std::string(strArg), std::string(strDefault));
}
int64_t GetArg(std::string_view strArg, int64_t nDefault)
{
return GetArg(std::string(strArg), nDefault);
}
bool GetBoolArg(std::string_view strArg, bool fDefault)
{
return GetBoolArg(std::string(strArg), fDefault);
}
bool SoftSetArg(std::string_view strArg, std::string_view strValue)
{
return SoftSetArg(std::string(strArg), std::string(strValue));
}
bool SoftSetBoolArg(std::string_view strArg, bool fValue)
{
return SoftSetBoolArg(std::string(strArg), fValue);
}
bool WildcardMatch(std::string_view str, std::string_view mask)
{
return WildcardMatch(std::string(str), std::string(mask));
}
string EncodeBase64(const unsigned char* pch, size_t len)
{
+4
View File
@@ -20,6 +20,7 @@
#include <string_view>
#include <sstream>
#include <algorithm>
#include <cstdio>
#include <chrono>
#include <thread>
@@ -190,6 +191,9 @@ bool ATTR_WARN_PRINTF(1,2) error(const char *format, ...);
#define printf OutputDebugStringF
void LogException(std::exception* pex, const char* pszThread);
// LogPrintf - variadic macro for logging to stderr (C++20 modernization: restored from removed definition)
#define LogPrintf(...) fprintf(stderr, __VA_ARGS__)
void PrintException(std::exception* pex, const char* pszThread);
void PrintExceptionContinue(std::exception* pex, const char* pszThread);
void ParseString(std::string_view str, char c, std::vector<std::string>& v);
+168 -15
View File
@@ -8,6 +8,12 @@
#include "checkpoints.h"
#include "util.h"
#include "ui_interface.h"
#include "addressindex.h"
#include <variant>
// defined in main.cpp
extern bool fAddressIndex;
#include <filesystem>
@@ -36,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
@@ -85,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);
@@ -95,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
@@ -176,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);
@@ -183,20 +239,45 @@ 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;
}
// Extract (type, hash160) from a scriptPubKey for the address index.
// Mirrors GetAddressFromScript() in main.cpp (which is file-static there).
static bool SnapAddressFromScript(const CScript& script, int& nType, uint160& hashBytes)
{
CTxDestination dest;
if (!ExtractDestination(script, dest))
return false;
if (const CKeyID* keyId = std::get_if<CKeyID>(&dest)) {
nType = ADDR_TYPE_P2PKH; hashBytes = *keyId; return true;
}
if (const CScriptID* scriptId = std::get_if<CScriptID>(&dest)) {
nType = ADDR_TYPE_P2SH; hashBytes = *scriptId; return true;
}
return false;
}
// ---------------------------------------------------------------------------
// LoadSnapshot - load a UTXO snapshot into a fresh LevelDB
//
// `requireCheckpoint` controls whether the snapshot's tip block must be a
// known checkpoint. This gate exists to prevent malicious P2P peers from
// tricking the daemon into accepting a fake UTXO set at an arbitrary
// height on an alternate chain. Local file loads (operator already has
// filesystem access, so the trust model is the same as editing the chain
// state directly) skip the gate via requireCheckpoint=false. P2P-delivered
// snapshots (SnapshotNet) keep the gate on.
// ---------------------------------------------------------------------------
bool LoadSnapshot(const fs::path& snapshotPath,
const fs::path& /*dataDir — unused; resolved per-backend via GetChainDataDir()*/,
std::string& strError)
std::string& strError,
bool requireCheckpoint)
{
FILE* file = fopen(snapshotPath.string().c_str(), "rb");
if (!file) {
@@ -209,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 ||
@@ -219,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;
}
@@ -252,8 +345,9 @@ bool LoadSnapshot(const fs::path& snapshotPath,
return false;
}
// Verify snapshot block is a known checkpoint
if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) {
// Verify snapshot block is a known checkpoint (only for P2P-delivered
// snapshots — local files are operator-trusted and can be at any height)
if (requireCheckpoint && !Checkpoints::IsKnownCheckpoint(height, blockHash)) {
fclose(file);
strError = "Snapshot block " + blockHash.ToString() + " at height "
+ std::to_string(height) + " is not a known checkpoint";
@@ -282,6 +376,20 @@ bool LoadSnapshot(const fs::path& snapshotPath,
bool success = true;
unsigned int nBatchSize = 0;
// CRITICAL: Set fSerializeChainTrust=true before writing CDiskBlockIndex records.
// LoadBlockIndex later reads with fSerializeChainTrust=true (derived from
// dbformat >= 2), so writes must include nChainTrust to match. Without this,
// every LoadSnapshot is followed by an "end of data: iostream error" in
// LoadBlockIndex because the reader expects a field the writer omitted.
//
// The default value is false; nothing else in the daemon sets it to true
// BEFORE LoadSnapshot runs (only the in-place upgrade path inside
// LoadBlockIndex sets it true, which is too late). The snapshot writer
// (an external daemon or our own DumpSnapshot) may have set it differently;
// but for a fresh LevelDB created by LoadSnapshot, we want the resulting
// DB to be self-consistent, so we always write with the field included.
CDiskBlockIndex::fSerializeChainTrust = true;
if (!txdb.TxnBegin()) {
fclose(file);
strError = "Failed to begin chain DB transaction";
@@ -383,6 +491,19 @@ bool LoadSnapshot(const fs::path& snapshotPath,
strError = "WriteUtxo failed at index " + std::to_string(i);
break;
}
// Address index: snapshot UTXOs are all unspent -> credit balance + record UTXO.
if (::fAddressIndex && !entry.scriptPubKey.empty() && entry.nValue != 0) {
int nAType; uint160 aHash;
if (SnapAddressFromScript(entry.scriptPubKey, nAType, aHash)) {
txdb.WriteAddressUtxo(nAType, aHash, txhash, nIndex,
entry.nValue, entry.nHeight, entry.scriptPubKey);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal += entry.nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
}
}
nBatchSize++;
if (nBatchSize >= 50000) {
@@ -401,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;
@@ -446,6 +597,8 @@ bool LoadSnapshot(const fs::path& snapshotPath,
printf("UtxoSnapshot: successfully loaded %d headers + %d UTXOs at height %d\n",
numHeaders, numUtxos, height);
fLoadedFromSnapshot = true;
return true;
}
+6 -2
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)
@@ -29,10 +29,14 @@ namespace UtxoSnapshot {
// Load a UTXO snapshot from a file into a fresh LevelDB.
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
// The LevelDB must NOT be open yet (call before LoadBlockIndex).
// `requireCheckpoint` enforces that the snapshot tip is a known
// checkpoint (for P2P-delivered snapshots). Local loads from a
// trusted operator pass false.
// Returns true on success, sets strError on failure.
bool LoadSnapshot(const std::filesystem::path& snapshotPath,
const std::filesystem::path& dataDir,
std::string& strError);
std::string& strError,
bool requireCheckpoint);
} // namespace UtxoSnapshot
+102 -8
View File
@@ -7,11 +7,14 @@
#include "wallet.h"
#include "walletdb.h"
#include "crypter.h"
#include "hdwallet.h"
#include "ui_interface.h"
#include "base58.h"
#include "kernel.h"
#include "coincontrol.h"
#include "addressindex.h"
#include "util.h"
#include <cstring>
#include <memory>
#include <algorithm>
#include <random>
@@ -128,7 +131,12 @@ CPubKey CWallet::GenerateNewKey()
RandAddSeedPerfmon();
CKey key;
key.MakeNewKey(fCompressed);
bool fUsedHD = false;
if (fHDEnabled && !hdMnemonic.empty()) {
if (DeriveHDKey(nHDChainIndex, key)) { fUsedHD = true; fCompressed = true; }
}
if (!fUsedHD)
key.MakeNewKey(fCompressed);
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
@@ -144,6 +152,11 @@ CPubKey CWallet::GenerateNewKey()
if (!AddKey(key))
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed");
if (fUsedHD) {
nHDChainIndex++;
if (fFileBacked)
CWalletDB(strWalletFile).WriteHDChain(nHDChainIndex);
}
return key.GetPubKey();
}
@@ -208,6 +221,9 @@ bool CWallet::Lock()
if (fDebug)
printf("Locking wallet.\n");
if (IsCrypted())
hdMnemonic.clear(); // keep only the encrypted copy while locked
{
LOCK(cs_wallet);
CWalletDB wdb(strWalletFile);
@@ -232,8 +248,14 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
return false;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false;
if (CCryptoKeyStore::Unlock(vMasterKey))
if (CCryptoKeyStore::Unlock(vMasterKey)) {
if (fHDEnabled && hdMnemonic.empty() && !vchCryptedHDMnemonic.empty()) {
CSecret sec;
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec))
hdMnemonic.assign(sec.begin(), sec.end());
}
return true;
}
}
SecureMsgWalletUnlocked();
@@ -406,6 +428,15 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
return false;
}
if (fHDEnabled && !hdMnemonic.empty()) {
CSecret sec(hdMnemonic.begin(), hdMnemonic.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { dbEnc->TxnAbort(); return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
dbEnc->WriteHDCryptedMnemonic(iv, cipher);
}
SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
if (!dbEnc->TxnCommit())
@@ -483,7 +514,7 @@ void CWallet::WalletUpdateSpent(const CTransaction &tx, bool fBlock)
if (!IsInitialBlockDownload())
{
try { NotifyTransactionChanged(this, txin.prevout.hash, CT_UPDATED); }
catch (...) { LogPrintf("WARNING: NotifyTransactionChanged exception in WalletUpdateSpent\n"); }
catch (...) { printf("WARNING: NotifyTransactionChanged exception in WalletUpdateSpent\n"); }
}
}
}
@@ -504,7 +535,7 @@ void CWallet::WalletUpdateSpent(const CTransaction &tx, bool fBlock)
if (!IsInitialBlockDownload())
{
try { NotifyTransactionChanged(this, hash, CT_UPDATED); }
catch (...) { LogPrintf("WARNING: NotifyTransactionChanged exception in WalletUpdateSpent\n"); }
catch (...) { printf("WARNING: NotifyTransactionChanged exception in WalletUpdateSpent\n"); }
}
}
}
@@ -2162,7 +2193,7 @@ bool CWallet::CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey)
coin.MarkSpent(txin.prevout.n);
coin.WriteToDisk();
try { NotifyTransactionChanged(this, coin.GetHash(), CT_UPDATED); }
catch (...) { LogPrintf("WARNING: NotifyTransactionChanged exception in CommitTransaction\n"); }
catch (...) { printf("WARNING: NotifyTransactionChanged exception in CommitTransaction\n"); }
}
if (fFileBacked)
@@ -2292,7 +2323,7 @@ bool CWallet::SetAddressBookName(const CTxDestination& address, const string& st
SecureMsgWalletKeyChanged(caddress.ToString(), strName, nMode);
}
try { NotifyAddressBookChanged(this, address, strName, fOwned, nMode); }
catch (...) { LogPrintf("WARNING: NotifyAddressBookChanged exception in SetAddressBookName\n"); }
catch (...) { printf("WARNING: NotifyAddressBookChanged exception in SetAddressBookName\n"); }
if (!fFileBacked)
return false;
@@ -2315,7 +2346,7 @@ bool CWallet::DelAddressBookName(const CTxDestination& address)
SecureMsgWalletKeyChanged(caddress.ToString(), sName, CT_DELETED);
}
try { NotifyAddressBookChanged(this, address, "", fOwned, CT_DELETED); }
catch (...) { LogPrintf("WARNING: NotifyAddressBookChanged exception in DelAddressBookName\n"); }
catch (...) { printf("WARNING: NotifyAddressBookChanged exception in DelAddressBookName\n"); }
if (!fFileBacked)
return false;
@@ -2794,7 +2825,7 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx)
if (auto mi = mapWallet.find(hashTx); mi != mapWallet.end() && !IsInitialBlockDownload())
{
try { NotifyTransactionChanged(this, hashTx, CT_UPDATED); }
catch (...) { LogPrintf("WARNING: NotifyTransactionChanged exception in UpdatedTransaction\n"); }
catch (...) { printf("WARNING: NotifyTransactionChanged exception in UpdatedTransaction\n"); }
}
}
}
@@ -2847,3 +2878,66 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const {
}
// ---- HD wallet (BIP39/BIP32) implementation ----
bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const
{
if (hdMnemonic.empty())
return false;
unsigned char priv[32];
if (!hd::DeriveTriangles(hdMnemonic, "", 0, 0, (uint32_t)index, priv))
return false;
CSecret secret(priv, priv + 32);
memset(priv, 0, sizeof(priv));
keyOut.SetSecret(secret, true); // HD keys are compressed
return true;
}
bool CWallet::GetHDMnemonic(std::string& mnemonicOut) const
{
if (!fHDEnabled || hdMnemonic.empty())
return false;
mnemonicOut = hdMnemonic;
return true;
}
bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passphrase, bool fGenerate, std::string& mnemonicOut, std::string& strError)
{
LOCK(cs_wallet);
if (IsLocked()) { strError = "Wallet is locked; unlock it before setting an HD seed."; return false; }
std::string m = mnemonicIn;
if (m.empty()) {
if (!fGenerate) { strError = "No mnemonic supplied."; return false; }
m = hd::GenerateMnemonic(256);
if (m.empty()) { strError = "Failed to generate mnemonic."; return false; }
}
if (!hd::CheckMnemonic(m)) { strError = "Invalid mnemonic (unknown word or bad checksum)."; return false; }
unsigned char priv[32];
if (!hd::DeriveTriangles(m, passphrase, 0, 0, 0, priv)) { strError = "Key derivation failed."; return false; }
memset(priv, 0, sizeof(priv));
hdMnemonic = m;
fHDEnabled = true;
nHDChainIndex = 0;
if (fFileBacked) {
CWalletDB wdb(strWalletFile);
if (IsCrypted()) {
CSecret sec(m.begin(), m.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
wdb.WriteHDCryptedMnemonic(iv, cipher);
} else {
wdb.WriteHDMnemonic(m);
}
wdb.WriteHDChain(nHDChainIndex);
}
// Replace any pre-existing (random) keypool with HD-derived keys so that
// getnewaddress immediately hands out deterministic m/44'/2222'/0'/0/i keys.
NewKeyPool();
mnemonicOut = m;
return true;
}
+17
View File
@@ -105,6 +105,8 @@ public:
fFileBacked = false;
nMasterKeyMaxID = 0;
pwalletdbEncryption = nullptr;
fHDEnabled = false;
nHDChainIndex = 0;
nOrderPosNext = 0;
nCachedStakeWeight = 0;
nCachedStakeWeightTime = 0;
@@ -124,6 +126,13 @@ public:
CPubKey vchDefaultKey;
int64_t nTimeFirstKey;
// ---- HD (BIP39/BIP32) wallet state ----
bool fHDEnabled; // an HD seed has been set
int64_t nHDChainIndex; // next external index (m/44'/2222'/0'/0/n)
std::string hdMnemonic; // in-memory phrase (present when unlocked/unencrypted)
std::vector<unsigned char> vchCryptedHDMnemonic; // encrypted phrase (loaded, decrypted on unlock)
uint256 hdMnemonicIV; // IV for the encrypted phrase
// check whether we are allowed to upgrade (or already support) to the named feature
bool CanSupportFeature(WalletFeature wf) { return nWalletMaxVersion >= static_cast<int>(wf); }
@@ -133,6 +142,14 @@ public:
// keystore implementation
// Generate a new key
CPubKey GenerateNewKey();
// ---- HD wallet (BIP39/BIP32) ----
bool IsHDEnabled() const { return fHDEnabled; }
bool SetHDSeed(const std::string& mnemonicIn, const std::string& passphrase, bool fGenerate, std::string& mnemonicOut, std::string& strError);
bool GetHDMnemonic(std::string& mnemonicOut) const;
bool DeriveHDKey(int64_t index, CKey& keyOut) const;
bool LoadHDMnemonic(const std::string& m) { hdMnemonic = m; fHDEnabled = true; return true; }
bool LoadCryptedHDMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) { hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; fHDEnabled = true; return true; }
// Adds a key to the store, and saves it to disk.
bool AddKey(const CKey& key);
// Adds a key to the store, without saving it to disk (used by LoadWallet)
+20 -1
View File
@@ -429,6 +429,24 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
pwallet->mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime);
}
else if (strType == "hdmnemonic")
{
std::string m;
ssValue >> m;
pwallet->LoadHDMnemonic(m);
}
else if (strType == "hdcmnemonic")
{
std::pair<uint256, std::vector<unsigned char> > cm;
ssValue >> cm;
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
}
else if (strType == "hdchain")
{
int64_t n;
ssValue >> n;
pwallet->nHDChainIndex = n;
}
else if (strType == "version")
{
ssValue >> wss.nFileVersion;
@@ -461,7 +479,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
static bool IsKeyType(string strType)
{
return (strType== "key" || strType == "wkey" ||
strType == "mkey" || strType == "ckey");
strType == "mkey" || strType == "ckey" ||
strType == "hdmnemonic" || strType == "hdcmnemonic");
}
DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
+15
View File
@@ -169,6 +169,21 @@ public:
return Write(std::string("defaultkey"), vchPubKey.Raw());
}
bool WriteHDMnemonic(const std::string& mnemonic) {
nWalletDBUpdated++;
Erase(std::string("hdcmnemonic"));
return Write(std::string("hdmnemonic"), mnemonic);
}
bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++;
Erase(std::string("hdmnemonic"));
return Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher));
}
bool WriteHDChain(int64_t nIndex) {
nWalletDBUpdated++;
return Write(std::string("hdchain"), nIndex);
}
bool ReadPool(int64_t nPool, CKeyPool& keypool)
{
return Read(std::make_pair(std::string("pool"), nPool), keypool);