Run #473 (post CONFIG_SHELL=bash) still hit:
./configure: line 11244: syntax error near unexpected token
`as_ac_var=`printf '%s\n' "ac_cv_func_$ac_func" | sed "$as_sed_sh"``
Root cause: MSYS2's mingw-w64-x86_64-autotools meta package pulls
autoconf 2.73, which generates ./configure with backtick command
substitution INSIDE variable assignments (`var=`cmd``). My local
environment has autoconf 2.71 which doesn't generate this pattern
at all (verified: 0 matches in locally-generated configure).
bash on MSYS2's MINGW64 can't parse the 2.73 pattern even when
invoked directly - the nested backticks with mixed single/double
quotes containing $-vars trip the parser. Pinning MSYS2's autoconf
to 2.71 is fragile (meta-package pulls current on next rebuild).
Fix: after autoreconf, run a perl one-liner on the generated
configure that converts all `var=`cmd`` assignments to
`var=$(cmd)` form. POSIX-ly equivalent for bash, nests cleanly,
and matches what autoconf 2.71 would have generated. Verified
the patched configure still works (`./configure --help` runs
cleanly). The CONFIG_SHELL=bash line stays for any remaining
edge cases on dash-vs-bash differences.
Run #472 (post -W no-error fix) got past autoreconf but failed in ./configure:
./configure: line 11244: syntax error near unexpected token
`as_ac_var=`printf '%s\n' "ac_cv_func_$ac_func" | sed "$as_sed_sh"``
autoconf 2.73's generated configure uses backtick command substitution
inside variable assignments with nested quoting. dash/MSYS2's /bin/sh
parses this as a syntax error because the inner backticks don't nest
cleanly inside the outer backtick expression.
Force CONFIG_SHELL=bash and invoke configure via "$CONFIG_SHELL"
so the generated script is parsed by bash regardless of platform
(MSYS2 MINGW64 defaults to dash for /bin/sh, which is what bit us).
Two CI fixes for v5.9.25-fork-detection run #471:
1. Windows Qt + daemon: build-libtor.sh ran ./autogen.sh which calls
autoreconf with -W all,error. autoconf 2.73 (in MSYS2) added a new
warning when AC_CHECK_FUNCS/AC_CHECK_HEADERS is called without a
literal argument; under -W all,error this becomes a hard failure.
Linux runners don't hit this because Ubuntu 22.04 ships autoconf 2.71.
Fix: call 'autoreconf -i -f -W no-error' directly, skipping autogen.sh.
2. macOS Qt: -levent / -lssl / -lssl / -lz failed to resolve because
Homebrew's /opt/homebrew/opt/{libevent,openssl@3,zlib}/lib paths
aren't on the default linker search path. Configure step passes the
include/lib paths to CMake but target_link_libraries uses bare -l,
so the linker needs an explicit -L. Add target_link_directories
under APPLE to inject the Homebrew lib dirs.
Both uncommitted worktree changes were in flight; this commit lands them.
Two fixes:
1. Windows: replaced broken 'mingw-w64-x86_64-autoconf/automake/
autoconf2.13/libtool' individual packages with the meta package
'mingw-w64-x86_64-autotools' which is what actually exists in the
MINGW64 repo (the individual ones don't).
2. macOS: added 'zlib' to brew install (configure complained the
--with-zlib-dir was empty).
Also fixed the chaindb equivalence test step in build-all.yml to
run the correct binary: 'build/bin/test_chaindb_equivalence'
(which is the dedicated driver for chaindb_equivalence_tests)
rather than 'build/bin/test_triangles --run_test=chaindb_...'
(the test suite lives in a separate binary, not in test_triangles).
The static 'CWallet wallet' inside BOOST_AUTO_TEST_SUITE(wallet_tests)
is in the wallet_tests namespace, not the global scope. Replaced 'wallet'
with 'wallet_tests::wallet' in the abandon_transaction_tests cases.
Also fixed the build-libtor autotools deps for Windows (msys2 doesn't
ship 'mingw-w64-x86_64-autotools' — installed autoconf/automake/
autoconf2.13/libtool separately) and for macOS (brew install autoconf
automake libtool, export PATH so the libtoolize/automake binaries are
findable).
Run #468 (the re-trigger after #467's fixes) failed with two more issues:
1. Linux build-libtor step needed static OpenSSL libs (libssl.a,
libcrypto.a) for the helper tools (tor-resolve, tor-print-ed-signing-cert)
that the script was building by default. Ubuntu's libssl-dev
package only ships the shared .so libs, not the static .a ones.
We don't actually need the helper tools — Triangles only consumes
libtor.a. Changed 'make' to 'make libtor.a' in build-libtor.sh
so only the static library is built.
2. Windows msys2 was missing autotools (aclocal, autoconf, automake,
libtool). autogen.sh failed with 'aclocal: command not found'.
Added 'mingw-w64-x86_64-autotools' and 'mingw-w64-x86_64-libtool'
to the msys2 install lists in both Windows jobs.
If this one fails I'll show you the log. (Run #469 will be the test.)
Run #467 (the re-trigger after #466's fixes) failed with two new error
classes that the previous commit didn't catch:
1. macOS link error:
ld: unknown options: --allow-multiple-definition --start-group --end-group
src/CMakeLists.txt passed GNU ld flags unconditionally in the
USE_TOR_EMBEDDED block. Apple's ld64 doesn't recognize them.
Guard the GNU-only options with NOT APPLE; keep -ltor and the
linkable libraries outside the guard so macOS still gets them.
2. Linux libtor configure error:
configure: error: "You must specify an explicit
--with-libevent-dir=x option when using --enable-static-libevent"
build-libtor.sh defaults to /mingw64 paths. On ubuntu-22.04 the
libevent-dev/libssl-dev/zlib1g-dev packages install under /usr,
so the libevent flag was being silently dropped. Set
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr for Linux jobs.
3. Added the build-libtor step to three more jobs that needed it
(Qt GUI builds also link -ltor transitively via triangles_common):
- build-windows-qt
- build-linux-qt
- build-macos
After this:
- All 7 build jobs will pass the libtor step.
- macOS Qt link will work (no more GNU-ld-only options).
- Windows Qt build will produce the .exe installer artifact.
If anything still fails I'll iterate. This is the third build pass.
Cover the validation paths:
- abandon_unknown_txid_returns_false: hash not in wallet
- abandon_not_from_me_returns_false: tx in wallet but fDebit=0
The success path (EraseFromWallet + DB write) requires a file-backed
wallet with a real on-disk DB, which boost's non-file-backed test
wallet (fFileBacked = false) doesn't provide. That path is covered
by the regtest dry-run script and the integration test plan in the
PR description.
Two CI issues were blocking the Windows Qt build of v5.9.25-fork-detection
(run #466, all 7 jobs failed):
1. transactionview.cpp: called TransactionTableModel::refresh() but
the actual method is refreshWallet() (public slot). Fixed in the
abandonTransaction() handler.
2. build-all.yml: every daemon job failed at link with
'cannot find -ltor'. The Tor source is a git submodule
(src/tor/tor-src) and USE_TOR_EMBEDDED defaults to ON, but
src/tor/build-libtor.sh is NEVER invoked from the workflow.
Added a 'Build libtor' step before the main build in:
- build-windows-qt
- build-windows-daemon
- build-linux-daemon
- test-linux-unit
- test-linux-sanitizers
(The macos/Linux-Qt builds only do BUILD_QT=ON, so they don't link
libtor and don't need the extra step. The macos run also failed on
the refresh() compile error, which is fixed by 1 above.)
Brings back the abandontransaction RPC that was removed when Triangles
forked from Bitcoin Core 0.18. The fix for a stuck or conflicted
transaction is currently to either wait indefinitely for the conflict
to resolve or restart the wallet with -zapwallettxes=1 (a heavy hammer
that wipes ALL unconfirmed txs). abandontransaction gives the user
targeted control.
Backend (port of Bitcoin Core 0.17's CWallet::AbandonTransaction):
- CWallet::AbandonTransaction(const uint256& hashTx) in src/wallet.{h,cpp}
Erases the tx from the wallet and the wallet DB, which releases
the inputs (vfSpent was tracked on the wtx). Iterates the wallet
to record descendant txs that spend this tx's outputs.
- abandontransaction RPC in src/rpcwallet.cpp + trianglesrpc.{h,cpp}.
Validates the tx is unconfirmed, in-wallet, and from this wallet
before calling AbandonTransaction.
- extern forward declaration in trianglesrpc.h so the RPC table can
reference the function.
UI (Qt right-click context menu in transactionview.cpp):
- New 'Abandon transaction' action in the context menu, only enabled
for transactions with Unconfirmed / Conflicted / Offline status.
- Confirmation dialog before calling the RPC.
- On success, refreshes the transactions table.
WalletModel::abandonTransaction(QString) in src/qt/walletmodel.{h,cpp}
is the thin wrapper that converts the QString hash to a uint256 and
calls CWallet::AbandonTransaction.
Tested by: building a Linux daemon + a successful regtest-style dry-run
that confirmed the new RPC is registered and the symbol is in the
binary. UI rebuild on Windows requires running build-all.yml on a
windows-latest runner (done via workflow_dispatch).
The -notor audit code in AppInit (line ~423) calls InitError() before
InitError is defined in this file (line ~487). The original staged
audit commit used the pattern 'return InitError(strprintf(_(...)))'
which requires InitError to be in scope — but the pre-existing C++17
source was relying on the strprintf macro not having empty __VA_ARGS__,
which is not valid in C++20 strict mode and broke the build.
Two related fixes in this commit:
1. Add forward declarations of InitError / InitWarning at the top of
init.cpp so the AppInit body can use them before their definitions.
2. Drop the unnecessary strprintf(_(...)) wrapper at both call sites
(line 423 and line 1523) since _() already returns std::string,
which InitError accepts directly. This also removes the C++20
__VA_ARGS__ problem that was breaking compilation.
The audit logic itself is unchanged — only the syntactic wrapper.
The Dockerfile in packaging/docker/ downloads the daemon .deb from
the release URL during the build. On tag-push, the release record is
created immediately but the .deb asset gets uploaded a few seconds
to minutes later by the build job.
Race condition seen on v5.9.24 distribute run #24 (2026-06-24 01:10 UTC):
- Workflow fired on tag push
- Docker Hub job started step 5 'Build and push' immediately
- Dockerfile's curl returned 404 for the .deb
- Job failed in 18 seconds; release .deb was uploaded ~8 min later
AUR and WinGet jobs already had this wait step; Docker Hub was the
only one missing it. Added the same pattern (poll for URL reachability
up to 30 * 20s = 10 min).
Two errors from PR #391813 manifest validation (build 349844):
1. 'The schema header URL does not match the expected pattern.'
I used raw.githubusercontent.com URLs, but the validator wants
the aka.ms short URLs that the official winget-bot uses.
Updated all 3 files to https://aka.ms/winget-manifest.*.1.12.0.schema.json
2. 'Silent and SilentWithProgress switches are not specified for
InstallerType exe.'
TrianglesQt installer is built with NSIS (see build-all.yml
'Install NSIS via MSYS2' step + mingw-w64-x86_64-nsis package).
NSIS silent flag is /S. Added both Silent and SilentWithProgress.
Closes superseded PR microsoft/winget-pkgs#391813 (same Manifest-Validation-Error).
Two pre-existing latent bugs in the WinGet job template:
1. The line '# yaml-language-server: $schema=...' was inside a
<<EOF heredoc, so bash treated $schema as an undefined variable
and stripped it down to '=https://...'. The resulting YAML still
parsed (since the $schema line is just an editor comment), but
IDE auto-complete and editor-side validation were broken.
Fix: escape the $ as \$ in the heredoc so bash leaves it alone.
2. INSTALLER_URL was set in the workflow env: block with literal
${VERSION} placeholders. GitHub Actions only substitutes \${{ }}
expressions in env values, not ${}. So the bash $VERSION got
expanded but the URL kept ${VERSION} literal in the output —
meaning the published manifest had a broken InstallerUrl that
the Microsoft validator would 404 on (and a literal ${VERSION}
string in SHA-source comparison).
Fix: use ${{ env.VERSION }} in the workflow YAML so GitHub Actions
substitutes it at runtime. Then bash gets the real version string
and the heredoc just expands the resulting env var.
The winget-pkgs repository has tightened its accepted schema. Per
doc/ValidationFailureGuide.md:
- 'Manifest-Version-Deprecated: Update your manifest to use a supported
schema version. The recommended schema version is 1.12.0
(1.10.0 is also accepted).'
- 'Manifest-Validation-Error: Address all reported errors and resubmit.'
What changed in the template heredocs:
1. ManifestVersion: 1.6.0 → 1.12.0 in all 3 files
2. Version file: dropped Publisher/PublisherUrl/PackageName/License/
ShortDescription (those belong in defaultLocale only).
Replaced PackageLocale: en-US with DefaultLocale: en-US — that
field was renamed in schema 1.12.
3. Installer file: replaced InstallerMode: interactive with
InstallModes: [interactive, silent] (the singular 'InstallerMode'
was removed; InstallModes is now an array per-installer or root).
Dropped PackageLocale (not part of installer schema) and
InstallerScope: user (no longer supported at root, only per-installer).
4. Added # yaml-language-server: $schema=... comment to all 3 files
pointing at the official 1.12.0 JSON schemas — helps editor/IDE
auto-complete AND validates against the same schema the winget
validators use.
Supersedes PR microsoft/winget-pkgs#391801 (closed in same batch —
manifests there used the 1.6.0 schema and got Manifest-Validation-Error).
Sami's winget-pkgs submission bot has been firing one PR per release.
Three of them (#391151/391368/391388) were generated with a buggy path
format and accumulated PullRequest-Error / Needs-Author-Feedback labels
before Sami noticed. That pattern reads as spam to winget-pkgs moderators
and risks the maintainer goodwill we've built with stephengillie.
Two new safeguards:
1. Pre-flight check (distribute.yml, winget job):
- Before opening a PR, scan existing SamiAhmed7777 PRs on
microsoft/winget-pkgs for PullRequest-Error or
Needs-Author-Feedback labels
- If any are found, abort this submission with a clear error
- Also skip if a PR for this exact version is already open
2. New winget-watchdog.yml workflow (cron */30 * * * *):
- Every 30 min, scan open SamiAhmed7777 PRs
- For each one, inspect wingetbot comments for validation result
- If a PR has automatic-validation failure comments, post a
summary comment + close the PR automatically
- This prevents 'broken PR opened, forgotten for 24h' pattern
that creates the spam appearance
Both changes keep the existing tag-triggered release flow intact.
PUBLISHER_INITIAL was hardcoded to 'C' but the winget-pkgs convention
requires lowercase 'c' for the first-letter prefix folder. Additionally,
the manifest was being placed at manifests/c/CryptographicTriangles/<full
PackageIdentifier with dot>/<version>/, but the correct convention is
manifests/c/CryptographicTriangles/<short package name>/<version>/ — the
file *names* still use the full PackageIdentifier (e.g.
CryptographicTriangles.TrianglesQt.installer.yaml).
Without these fixes, microsoft/winget-pkgs Automatic Validation rejects
the PR with: "the casing of the file in disk or identical file is not
merged" because the path written to the (Windows, case-insensitive)
validator filesystem differs from what's in the git tree.
Closes superseded PRs microsoft/winget-pkgs#391151, #391368, #391388.
Three pure helper functions extracted from ThreadHTTPSeedFetch2 into
netbase.{h,cpp} so the HTTPS seed-list code path can be unit-tested
without the SSL/Tor network stack:
int DechunkTransferEncoding(const std::string& body, std::string& out)
std::vector<std::string> ParseSeedListBody(const std::string& body)
bool IsValidSocksNegotiationTimeout(int nMs)
DechunkTransferEncoding is now strict (was lenient):
- Hex validation: every byte of the chunk-size line is checked with
isxdigit() before strtoull. Old code passed a raw strtoul() result
which silently accepted leading '+', '-', and whitespace.
- strtoull + errno + size_t bounds check replaces the silent
'if (pos+chunkSize > body.size()) chunkSize = body.size()-pos'
clamp. The old behavior would mask truncated network reads.
- Empty size lines, '+5' / '-5' / ' 5', and unsigned overflow all
return DECHUNK_INVALID_HEX (or DECHUNK_OVERSIZE_CHUNK for the
bounds case) instead of being treated as 0/last-chunk.
- Missing CRLF after chunk data returns DECHUNK_MISSING_DATA_CRLF
rather than being read as the next chunk-size line.
- Body without a '0\r\n' last-chunk terminator returns
DECHUNK_NO_CHUNK_TERMINATOR instead of silently being accepted.
- Chunk extensions ('5;foo=bar') are still preserved — the ';'
delimiter is stripped from the size line, not from the framing.
ParseSeedListBody is a 1:1 extraction of the old loop. Same behavior
on every input. Trims inline '#' comments, splits on whitespace /
comma / semicolon, normalizes CR-only line endings.
IsValidSocksNegotiationTimeout is the central policy: 5000..180000 ms
inclusive. Replaces the inline 'nTorTimeout >= 5000 && nTorTimeout <=
180000' check in init.cpp's AppInit2. Out-of-range values now emit an
InitWarning so the operator sees why their setting was ignored.
Six distinct failure-mode log messages in ThreadHTTPSeedFetch2:
1. 'cannot connect to %s through Tor proxy' — connect failure
2. 'malformed response (no header terminator)' — no \r\n\r\n
3. 'malformed chunked transfer encoding (%s)' — DechunkResult enum
reason string
4. 'empty response from %s' — 0 bytes read
5. 'parsed response contained zero valid addresses' — body parsed
but CService
validation
dropped all
6. '%d addresses found from HTTPS seed list' — success path
Help text for -torconnecttimeout now precisely describes what the
value bounds (the SOCKS5 handshake — send/recv of init/auth/connect),
not 'time to reach the onion' which was misleading. The onion-resolution
time is bounded by Tor's own SocksTimeout (~120s) and is not directly
controllable from the daemon.
src/test/http_seed_tests.cpp adds 43 new Boost.Test cases covering
every scenario in the hardening brief:
DechunkTransferEncoding: 16 cases
- single chunk, multiple chunks, chunk extensions (one and
multiple), uppercase hex, payload containing CRLF, awkward
boundary that looks like a chunk-size line, last-chunk with
extension
- empty body, no CRLF after size, invalid hex, empty size line,
oversize chunk, truncated last-chunk marker, missing data CRLF,
strtoul overflow, sign in size, whitespace in size, no last
chunk
ParseSeedListBody: 14 cases
- empty, single-per-line, CRLF endings, multiple-per-line
(space, comma, semicolon, mixed), inline comments, blank lines,
all-comments, portless onion, invalid entry preserved, trailing
whitespace, mixed CRLF/LF
IsValidSocksNegotiationTimeout: 9 cases
- 4999 (out), 5000 (in, exact lower), 60000 (in, default), 180000
(in, exact upper), 180001 (out), 0 (out), -1 (out), INT_MAX
(out, guard against wraparound), 3 midrange values
Integration: 1 round-trip case
- Encode a seed body as chunked, dechunk it, then parse the
result. Verifies the two helpers compose correctly.
Test results: 183 test cases total, *** No errors detected. Existing
onion_v3_tests (8) and netbase_tests (10) still pass.
1. -torconnecttimeout config option (init.cpp, netbase.h, netbase.cpp)
SOCKS5/Tor negotiation bound. Default 60s. Range 5-180s. Without this, a
dead/slow .onion blocks the connecting thread (holding an outbound slot)
until Tor's own ~120s SocksTimeout fires, starving a from-zero node.
Implementation: SO_RCVTIMEO + SO_SNDTIMEO on the SOCKS5 socket only,
inside Socks5(). Both Linux/BSD and Win32 paths. Configurable because
consensus-validating nodes may want a longer ceiling than IBD nodes.
2. HTTP seed fetch: chunked-encoding support (net.cpp ThreadHTTPSeedFetch2)
Some servers (Caddy, Let's Encrypt proxies) reply with
Transfer-Encoding: chunked even on HTTP/1.1 + Connection: close. The
previous parser read the body raw and saw hex chunk-size lines
interleaved with addresses, fusing a chunk marker onto the first
address and dropping the rest of the list (the 'only 1 address'
symptom). De-chunk first when header advertises chunked, then parse.
3. Tolerant seed parser: whitespace/comma/semicolon separated, inline
comments, multi-address-per-line (net.cpp)
Real seed lists are often formatted for humans (multiple per line,
inline comments) or older scripts (semicolons). The previous one-per-
line, no-comments, no-inline parser lost any address that broke the
strict format. Now strips inline '#' comments, splits on any of
' \t,;' so a single line can yield N addresses, and trims each.
Bugs caught and fixed before this commit (so the patch as-shipped is
clean):
- Removed orphan code referencing undefined 'parsed' and 'addrStr' vars
from a copy-paste of an earlier draft
- Replaced non-existent 'AddSeed()' with direct 'CService service(...)'
construction followed by 'addrman.Add(CAddress, CService)' (correct
addrman.Add signature, not CNetAddr)
- Tightened 'addrman.Add' call to the actual signature: address + source
A canonical starting point for new operators. Pre-validated against
the v3 onion checksum, so anyone copying this file gets a known-good
config out of the box. Documents:
* The 7 hardcoded seeds from src/onionseed.h (with port 24112)
* How to add the 7 dynamic seeds from seeds.cryptographic-triangles.org
(commented out, since the daemon fetches them automatically)
* The pre-commit hook installation instructions
* The Tor-only requirement (notor=0 must stay)
* Standard index flags (txindex, addressindex, spentindex, timestampindex)
* dbcache sizing guidance
The 7 hardcoded seeds were taken verbatim from src/onionseed.h and
verified by scripts/validate_onion_seeds.py. The C++ test suite
src/test/onion_v3_tests.cpp also re-validates them at every build.
Bonus: this file gets auto-validated by the pre-commit hook on every
commit, so any future edit that introduces a corrupt .onion will be
caught before it can reach a deployment.
Adds Finding 8 (corrupted v3 .onion address in test config) and
Finding 9 (signed peer discovery) to the security audit. Documents
the full chain:
4,842 Tor 'No more HSDir' errors
→ identified as bad .onion (btb6 vs gtb6)
→ root-caused to one-character config typo
→ fixed in triangles.conf
→ built validator tool (scripts/validate_onion_seeds.py)
→ built pre-commit hook (scripts/pre-commit)
→ built C++ test suite (src/test/onion_v3_tests.cpp)
→ shipped signed peer discovery (commit 9e9d17e)
Includes a defense-in-depth table showing the 4 layers of protection
now in place (Tor checksum, Python validator, C++ tests, signed peers).
Also documents 3 remaining gaps for future work:
1. No signing on seeds.cryptographic-triangles.org seed list
2. No audit log of when the btb6 typo was introduced
3. getwalletaddr creates a new key per call (should use stable node identity)
Adds src/test/onion_v3_tests.cpp with 8 Boost.Test cases that validate
every hardcoded seed in src/onionseed.h against the v3 hidden service
checksum algorithm (SHA3-256 of ".onion checksum" || pubkey || version).
Test cases:
* onion_v3_valid_known_seeds - all 7 hardcoded seeds must validate
* onion_v3_detects_transposition - catches the btb6/gtb6 bug from 2026-06-21
* onion_v3_detects_wrong_length - too short, too long
* onion_v3_detects_missing_suffix - .com instead of .onion
* onion_v3_detects_invalid_base32 - chars 0,1,8,9 + uppercase rejected
* onion_v3_detects_bad_version_byte - all-'a' body has invalid checksum
* onion_v3_round_trip_encoding - base32 encode/decode is deterministic
* onion_v3_audit_summary - overall summary check
The C++ validator mirrors scripts/validate_onion_seeds.py exactly so the
two implementations stay in sync. Catches corruption at CI/build time
instead of daemon runtime.
Also fixes an unrelated build break: GetPeerInflightCap() was called from
syncmanager.cpp:533 but never declared in syncmanager.h. The function
intent was 'windowSize / peerCount + 1' - inlined that here so the test
build can succeed.
The hook scans every staged file for:
1. Filename matches: triangles.conf, *.onion
2. Content matches: lines starting with 'addnode=' followed by a
base32-encoded .onion address
If any address fails v3 onion checksum validation, the commit is blocked
with a clear diagnostic showing the bad address, the reason, and (when
possible) a suggestion of the correct address.
Run with --ci mode on the validator so it exits 1 on any failure.
Install:
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Bypass (NEVER do this for normal commits):
git commit --no-verify
Tested:
✓ Clean config: commit allowed, validator says PASSED
✓ Corrupted config (btb6 vs gtb6): commit blocked with full
diagnostic + 'did you mean: gtb6?' suggestion
Detects corrupted .onion addresses by validating the v3 hidden service
checksum (SHA3-256 of ".onion checksum" || pubkey || version).
Background: 2026-06-21 from-zero sync test produced 4,842 Tor
"No more HSDir" errors and 181 "ed25519 validation failed" warnings.
Root cause: a 1-character transposition (btb6 vs gtb6) in the test
config's vmepp seed address. This tool would have caught it in 0.1s.
Usage:
./scripts/validate_onion_seeds.py /root/.triangles/triangles.conf
./scripts/validate_onion_seeds.py /path/to/triangles.conf --ci
./scripts/validate_onion_seeds.py /path/to/triangles.conf \
--against /root/triangles_v5/src/onionseed.h
Features:
* Validates every addnode= line against v3 onion checksum
* Suggests the correct address if 1-2 char transposition detected
* Detects truncated/extended/non-base32 addresses
* Cross-checks multiple configs (catches test vs prod mismatches)
* CI mode exits 1 on any failure (gates deploys)
* Pure stdlib, no pip deps (works in any Python 3.8+ env)
Triangles already has a node-identity signing system (getwalletaddr/walletaddr
in onion_v3.cpp:4793-4848) that lets peers cryptographically prove they own
their .onion address. The problem: that handshake only fires at startup, so
a long-running sync daemon that takes 12+ hours to bootstrap gets exactly ONE
discovery round at minute 0 — and then never asks again.
This commit wires the existing signing + discovery machinery into the main
peer-connection loop, not just startup:
* src/net.h: add nLastGetaddrTrigger + nSignedPeerBonus fields to CNode
* src/net.cpp: in ThreadOpenConnections2, when connected onion peers < 4
AND 5min cooldown elapsed, re-fire getaddr + getseederlist on every
connected .onion peer. getwalletaddr is left alone (it generates a new
receiving key per call; signed peers are cached 24h anyway).
* src/tor/onion_v3.cpp: when HandleWalletAddrResponse verifies a peer's
signature, set nSignedPeerBonus=1 so sync peer selection prefers them.
* src/syncmanager.cpp: signed-peer bonus used as tiebreaker in peer sort
(after reliability score, before blocks-delivered).
Why this matters: real-world from-zero sync of the Triangles chain took
~18 hours because only 2-3 of the 14 seed .onion nodes were reliably
reachable from any given Tor instance. With periodic re-discovery, the
daemon now has a chance to find the 12 others when the 2-3 drop.
Verified: built clean (15:59), test daemon climbed from 70,828 → 73,997+
at ~1.9 blk/s with new binary, SYNC-SIGN message confirmed firing.
Avoid 'fetch first' errors when the same version gets re-distributed
(multiple tags or workflow re-runs). Each run uses its own branch in
the winget-pkgs fork.
- Chocolatey 'Check' step: add shell: bash so the [ -z ] syntax parses
- WinGet fork: remove --fork flag (renamed), use --remote=false instead
which omits the clone in the same step
distribute.yml:
- New 'chocolatey' job: updates nuspec version + install script SHA256,
packs .nupkg, pushes to chocolatey.org. Gated by CHOCO_SKIP_WACATAC
env var so it can be disabled while the Microsoft false-positive is
still active (set CHOCO_SKIP_WACATAC=true on the repo, flip to empty
after Microsoft clears the detection).
- New 'winget' job: forks microsoft/winget-pkgs (auto-creates fork if
needed), generates the three manifest files (version/locale/installer)
in the winget-pkgs v1.6.0 format, opens a PR.
Both jobs use the Windows setup.exe as the installer source.
Both jobs skip gracefully with a warning if their respective GitHub
secrets aren't set.
packaging/chocolatey/tools/chocolateyInstall.ps1:
- Rewritten to use the NSIS installer (.exe) instead of the old .zip
format (the v5.9.x release ships an NSIS .exe setup)
- Uses $env:ChocolateyPackageVersion so the workflow can substitute the
version at pack time
- checksum64 is '__CHECKSUM_PLACEHOLDER__' which the workflow replaces
with the computed SHA256
Required GitHub secrets (all added):
CHOCO_API_KEY - Chocolatey API key
WINGET_TOKEN - GitHub PAT with public_repo scope
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