The previous commit had a literal '***' placeholder where the GitHub
Actions expression ${{ secrets.HOMEBREW_GITHUB_TOKEN }} should have
been. The workflow couldn't parse, so runs showed as 'failure' with
zero jobs and the display name fell back to the file path.
Fixed by writing the correct expression directly.
Observed the workflow firing on regular push-to-master events, not just
tag pushes. GitHub is sometimes over-eager about workflow re-runs on
commits that touch the workflow file. Add an explicit job-level guard
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
to all four jobs so the distribute jobs only run on tag pushes or
manual workflow_dispatch events.
New 'homebrew' job in distribute.yml:
- Waits for the macOS .dmg to be available on the GitHub release
- Computes the new SHA256
- Clones SamiAhmed7777/homebrew-triangles
- Updates version + sha256 in both Formula/triangles.rb and
Casks/cryptographic-triangles.rb
- Commits and pushes to main
- Skips gracefully with a warning if HOMEBREW_GITHUB_TOKEN is not set
Required GitHub secret: HOMEBREW_GITHUB_TOKEN (added)
GitHub Actions doesn't allow 'secrets' context in 'if:' conditionals,
only in 'env:'. Reworked the workflow to:
- Capture DOCKERHUB_TOKEN and AUR_SSH_KEY into env vars at job level
- Each step that needs a secret checks env.* and exits 0 with a
::warning:: annotation if not set
- Skipped steps display a final summary in the job log
Same behavior, just no parser errors.
New workflow .github/workflows/distribute.yml:
- Triggers on v* tag push (and workflow_dispatch for manual runs)
- Docker job: builds + pushes to samiahmed7777/trianglesd with both
:VERSION and :latest tags, plus a post-push smoke test
- AUR job: runs in archlinux container, downloads the release .debs,
updates PKGBUILD with new version + SHA256s, regenerates .SRCINFO
via makepkg, commits and pushes to AUR via SSH
- Both jobs skip gracefully (with a clear warning) if their respective
GitHub secrets aren't set, so the workflow can be merged and tested
before secrets are configured
- Waits up to 10 minutes for the build-all release artifacts to be
available (build-all and distribute run in parallel on the same tag)
Required GitHub secrets:
DOCKERHUB_TOKEN — Docker Hub access token (have in vault)
AUR_SSH_KEY — Private key of the AUR packager (~/.ssh/aur_key)
Docker:
- Dockerfile now extracts from cryptographic-triangles-daemon_5.9.20_amd64.deb
(release no longer ships raw linux-x64 binaries)
- Multi-stage build with .deb extraction
- Includes triangles-cli alongside trianglesd
- LD_LIBRARY_PATH wrapper for the bundled lib/ dir
AUR:
- Bump triangles-qt-bin to 5.9.20
- Switch from raw linux-x64 binary download (no longer published) to
extracting the official .deb packages
- Bundle version-pinned libs in /opt/triangles/lib
- Add triangles-cli to provides
From-zero sync test confirmed: chain advances past 15k freeze zone
to 17k+ with no stall. Build clean (149/149 Ninja targets).
132/132 unit tests pass.
Sync-freeze patch (original):
- Backpressure ceiling HEADER_FRONT_MAX_AHEAD=8000
- PruneHeaders protects live sync window (nProtectFloor)
- Hard-cap eviction from highest-height first
- Bridge-repair getheaders from connected tip via PathReachesChain
Additional fix:
- Skip PoW check on PoS headers (nonce=0) in AddHeaderNode
Block 1026 is PoS but within the 0-9000 PoW range — old code
rejected valid PoS headers and severed the chain at height 1025
Verified: from-zero no-snapshot sync reached block 17k+ past the
old 15k freeze zone. 132/132 unit tests pass.
FastImport removal in commit bdb7253 made the v2 UTXO snapshot the
canonical sync start. The legacy DownloadBootstrap() function still
attempted to fetch /triangles-bootstrap.tar.gz first, then fell back to
filelist.txt — which still contained tri-bootstrap.tar.gz. Both legacy
URLs return 404 (cleaned up 2026-06-19), so the wallet wasted a request
on a dead path before reaching the v2 snapshot URL.
Changes:
- DownloadBootstrap() no longer tries /triangles-bootstrap.tar.gz.
- Goes straight to filelist.txt → downloads the URL listed there (now
utxo-snapshot.bin only, after the bootstrap server fix).
- Removed unused ExtractTarGz() helper function (~110 lines).
- Kept DEFAULT_HOST in bootstrap.h — init.cpp still references it
for the SnapshotNet P2P fetch.
No version bump. v5.9.20 binary built locally; SHA
ad34764e28fb0c922a3f3570e830ba5707fdc2f7f7a11301e8c0f60356048fd3.
Bootstrap server fix landed first:
- /var/www/triangles-bootstrap/filelist.txt now contains only
'utxo-snapshot.bin' (was tri-bootstrap.tar.gz + triangles-bootstrap.tar.gz).
This means existing laptop wallets (no rebuild needed) will now read the
updated filelist.txt on next bootstrap attempt and go straight to the
v2 snapshot URL.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.'
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.)
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).
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.
* 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>
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.
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.
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.
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.
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.