PR #26 (the bootstrap trusted-publisher API) merged with broken master
on 2026-07-10. The CI gate that should have caught it was:
continue-on-error: true
...
ctest --output-on-failure || true
Both protections combined: continue-on-error ignored a non-zero exit,
and `|| true` flattened any failure to exit 0 anyway. Result: PR #26
landed broken, PR #27 (script fuzz) inherited the breakage, and the
next 7 push cycles spent debugging CI failures that should have been
caught at PR-merge time.
This commit:
1. Drops `continue-on-error: true` on test-linux-unit (the soft-gate)
2. Drops `|| true` from the ctest invocation
3. Adds explanatory comments pointing to the PR #26 incident
The job is now a real CI gate: a unit-test regression blocks the PR.
If a single test turns out to be flaky on the CI runner, we should
fix the test (it'll be flaky locally too) rather than weaken the gate.
Companion jobs (test-linux-sanitizers, test-fuzz-smoke) were already
blocking. This brings test-linux-unit in line with them.
Co-authored-by: Sami Ahmed <sami@sami-ahmed.net>
* simd: fix UBSan signed-shift UB in fft64 INNER macro
The INNER macro at src/simd.c:379 combines the low and high halves of
two FFT values with a multiplier:
((u32)((l) * (mm)) & 0xFFFFU) + ((u32)((h) * (mm)) << 16)
When (h)*(mm) is a negative s32, the (u32) cast recovers the bit
pattern (large positive number), but then << 16 operates on the
integer-promoted value (typically int on x86_64). UBSan flags this as
signed-shift of negative.
Fix: explicit (u32) cast inside the shift expression forces the shift
operand to unsigned (well-defined per C++20). Outer (u32) cast keeps
the result type consistent. Bit-equivalent at runtime; type-safe for
UBSan.
Mirrors the pattern Krystie applied in b9d06d5 for the same issue in
FFT8/FFT16 macros. Could not reproduce the trip in a standalone
100-trial test (Hash9's specific call pattern from CBlock::GetHash
may not be reproducible in isolation), but the macro is the same UB
class as already-fixed sites — fix by inspection per the
triangles-test-suite-audit skill.
Build verified: ninja test_triangles clean, all 234 test cases +
21720 assertions pass under sanitizers. Tests exercise Hash9 via
TestingSetup, so the FFT path is covered.
* test: add libFuzzer harness + EvalScript stress tests + CI fuzz job
Three pieces, one goal: expand coverage of script.cpp (the
consensus-critical opcode interpreter) beyond what Boost unit tests
catch.
1. libFuzzer harness (src/test/fuzz/)
Builds against the existing daemon object files (init.cpp.o,
wallet.cpp.o, noui.cpp.o) so we get the full CWallet vtable
without writing 100+ lines of fragile method stubs. Link line
reuses the sanitizer-friendly flags from test-linux-sanitizers.
Corpus seeded from src/test/data/script_{valid,invalid}.json
(1055 real Triangles scripts).
BUILD_FUZZ is OFF by default — gcc default build doesn't have
libFuzzer, so the flag gates the custom clang++ build cleanly.
2. Stress tests (src/test/script_stress_tests.cpp)
Six regression-guard tests for EvalScript's hard limits. Anyone
who removes a bound will get a test failure:
- deep_dup_stack_hits_opcount_limit — 250 OP_DUP rejected <1s
- max_keys_multisig_20_of_20 — 20-of-20 terminates <2s
- multisig_rejects_21_keys — nKeysCount > 20 rejected
- pushdata_over_520_rejected — MAX_SCRIPT_ELEMENT_SIZE
- script_size_over_10000_rejected — MAX_SCRIPT_SIZE
- disabled_opcodes_rejected — all 15 disabled opcodes
EvalScript contract caveat (captured in the test comments): on
false return, the stack is left dirty — inputs pushed before
rejection are still there. Tests assert stack.size() <= N for
N = number of inputs pushed, not the post-opcode expectation.
3. CI fuzz job (.github/workflows/build-all.yml)
test-fuzz-smoke job, sibling of test-linux-sanitizers. Reuses
the same runner + apt-get + RocksDB-from-source + Tor-from-source
steps so CI runtime doesn't double. Builds with clang-15,
ASan+UBSan+libFuzzer, seeds corpus, runs 5 minutes, fails only
on crash artifact (not on find_new_units=0 — libFuzzer always
writes .tmp churn during normal operation).
Verified:
- ninja test_triangles clean
- 234/234 test cases + 21720/21720 assertions pass
- 4/4 ctest suites pass (triangles_unit + chaindb_equivalence +
snapshotnet + chaindb_runtime)
- Local fuzz run: 8354 corpus files, ~5400 exec/sec, zero crashes
after several hours (-jobs=2 -workers=2)
* build: explicit <cassert> in allocators.h
clang's stricter include resolution surfaces the missing include even
though gcc tolerates it via some other transitive path. Without this,
PR #27 build with clang fails on assert() in LockedPageManager.
* fix(ci,fuzz): unbreak test-fuzz-smoke workflow + CMake fuzz link deps
Three bugs in PR #27's fuzz smoke integration that CI caught on first run:
1. CMAKE_EXE_LINKER_FLAGS pulled in '-fsanitize=fuzzer $SAN_FLAGS'. CMake's
compiler-probe linker test (used to verify the toolchain) doesn't define
LLVMFuzzerTestOneInput, so adding -fsanitize=fuzzer pulls in
libclang_rt.fuzzer's main() and trips 'multiple definition of `main`'.
Remove -fsanitize=fuzzer from global flags; fuzz_script already adds it
per-target via FUZZ_COMMON_FLAGS in src/CMakeLists.txt.
2. Workflow said --target script_fuzz / ./build-fuzz/bin/script_fuzz, but
the CMake target is fuzz_script (add_custom_target(fuzz_script ...)).
CI failed with 'unknown target'. Fixed in workflow + comment.
3. fuzz_script link references static libs at ${CMAKE_BINARY_DIR}/lib/
(libhash9_crypto.a, libleveldb_lib.a, libleveldb_memenv.a, libsecp256k1.a)
but didn't declare them as DEPENDS. First clean build races the link
step and fails with 'no such file or directory'. Added the four static
library targets to DEPENDS so ninja builds them first.
Verified locally: cmake configure clean, fuzz_script link succeeds,
binary runs (./bin/fuzz_script prints libFuzzer banner and reads corpus).
Tested with the same flag set CI uses (SAN_FLAGS with fuzzer-no-link,
BUILD_FUZZ=ON, clang-14).
* fix(ci,fuzz): make test-fuzz-smoke work end-to-end on clean builds
Three more bugs in PR #27's fuzz integration, caught on local repro
after commit 3239425 fixed the easy ones:
1. clang vs gcc warning mismatch (cmake/AddCompilerFlags.cmake):
gcc treats -Wreserved-user-defined-literal as a warning. clang-15+
in C++20 mode promotes it to an error and trips on hundreds of
Bitcoin-derived sites like strprintf("%"PRId64...) in util.cpp /
kernel.cpp. Conditional -Wno-reserved-user-defined-literal scoped
to clang only — gcc builds keep the original diagnostic.
2. secp256k1 ASM strictness (.github/workflows/build-all.yml):
Add -DSECP256K1_ASM=OFF to the fuzz configure. Clang-15+'s
register allocator is sometimes stricter than clang-14 about the
x86_64 inline asm in scalar_4x64_impl.h and fails with 'inline
assembly requires more registers than available'. The fuzz target
only needs ECC at the C-fallback level — slower but correct.
3. Empty .o glob at link time (src/CMakeLists.txt):
The fuzz link line referenced CMakeFiles/triangles_common.dir/*.o
and CMakeFiles/trianglesd.dir/*.o via file(GLOB), which evaluates
at cmake CONFIGURE time. On a fresh build dir, no .o files exist
yet → the link line was always empty → undefined references for
CKey::GetPubKey, typeinfo for CKeyStore, etc.
Replace the GLOB with a generated bash wrapper script
(fuzz_objs/link.sh) that does the find at link time, using bash
arrays to safely handle paths with spaces. The script is invoked
via ninja with the original link line as its argv; it prepends
the discovered .o files (excluding script.cpp.o — we have our
own clang-instrumented copy in fuzz_objs/) and exec's clang++.
Verified locally:
- cmake configures cleanly under clang-18 with the same flag set CI uses
- ninja fuzz_script links end-to-end (132 MB ELF, debug info, all
sanitizer coverage instrumentation intact)
- ./bin/fuzz_script runs and discovers coverage: 'INITED cov: 3
ft: 3 corp: 1/1b' from libFuzzer banner
- 4/4 ctest suites still pass on the gcc build (master chaindb /
snapshotnet / unit / equivalence)
This should make test-fuzz-smoke pass on the next CI run.
* fix(ci,fuzz): stabilize fuzz smoke job
* fix(ci,fuzz): portable fuzz build, exclude daemon main, stub globals
- Build daemon (init/wallet/noui) as an OBJECT library under BUILD_FUZZ
so the fuzz job does not pay the daemon executable link cost.
- Exclude init.cpp.o from the fuzz link wrapper (it defines the daemon's
main(), conflicting with libFuzzer's own).
- Replace hardcoded libboost/librocksdb filenames with -l flags and
Boost target paths resolved at configure time.
- Add -lubsan to the fuzz link line so libstdc++'s ubsan hooks resolve.
- Add a generated fuzz_stubs.cpp that defines pwalletMain, uiInterface,
CheckpointsMode, nNodeLifespan, etc. — every global that init.cpp
used to provide.
- Keep the CI workflow's libtor build step (fuzz links libtor.a).
Local verify: clang-15 + clang-18 + gcc all build fuzz_script; 15s
fuzz run completes 1913 execs with no crash artifacts; gcc test build
passes 4/4 ctest suites unchanged.
* fix(ci,fuzz): drop libi2pd*.a paths from fuzz link line
The fuzz job in build-all.yml runs libtor.sh but NOT libi2pd.sh, so
src/i2p/i2pd-src/libi2pd{,client,lang}.a don't exist on the CI runner.
The previous commit (720d711) hardcoded them into the fuzz link line;
the link failed with
clang: error: no such file or directory: '.../i2p/i2pd-src/libi2pd*.a'
i2p_embedded.cpp is already compiled into triangles_common, and with
USE_I2P_EMBEDDED=OFF (the CI default) the only i2p surface is the
no-op stub in triangles_common. So the .a references were both wrong
AND redundant.
Keep libtor.a: src/tor/build-libtor.sh IS run in the fuzz job, so the
file exists when the link wrapper invokes clang++.
Verified locally with clang-15 against the same cmake flags the
workflow uses: clean link, 12s fuzz run did 1239 executions, no crash
artifacts, libFuzzer reporting normal coverage growth.
* fix(ci,fuzz): install libgflags-dev for fuzz smoke job
CI failure on PR #27 test-fuzz-smoke: link step aborted with
`/usr/bin/ld: cannot find -lgflags`. The fuzz link line in
src/CMakeLists.txt references -lgflags (transitive dep of RocksDB),
and CI's ubuntu-22.04 runner does NOT ship libgflags-dev.
DNS2 ships libgflags-dev as an automatic dep of build-essential,
which is why local dry-runs didn't catch this.
Verified locally on DNS2:
- Cloned the exact CI cmake invocation (build-fuzz-verify dir)
- cmake -B + cmake --build --target fuzz_script: clean build
- 30s fuzz pass: 1058 inputs, 1935 features covered, no crashes
The libtor build step also depends on gflags transitively; making
it explicit in the apt-get list future-proofs both paths.
* fix(ci,fuzz): link -lrocksdb unconditionally in fuzz target
CI's test-fuzz-smoke job was failing with a torrent of
`undefined reference to rocksdb::Status::ToString[abi:cxx11]()`
errors after the libgflags fix landed. The fuzz target's RocksDB
link arg was:
$<IF:$<TARGET_EXISTS:RocksDB::rocksdb>,-lrocksdb,${ROCKSDB_LIBRARY}>
That generator expression was wrong on CI's exact code path:
1. CMake's `find_package(RocksDB CONFIG)` does NOT find the .cmake
config RocksDB 8.9.1 ships — only the .pc file.
2. `pkg_check_modules(RocksDB IMPORTED_TARGET)` therefore exposes
`PkgConfig::RocksDB` (NOT `RocksDB::rocksdb`), so
$<TARGET_EXISTS:RocksDB::rocksdb> is FALSE.
3. The fallback ${ROCKSDB_LIBRARY} is set ONLY inside the manual
`find_library()` probe at top-level CMakeLists.txt:170-190, which
is skipped when EITHER `RocksDB::rocksdb` or `PkgConfig::RocksDB`
already exists.
Result on CI: an empty string landed in the link line, so the link
step saw no `-lrocksdb` arg and every RocksDB symbol the fuzz
binary referenced became undefined.
Fix: just use a bare `-lrocksdb` and let the library search path
do the work. `build-rocksdb.sh` installs to /usr/local/lib (CI);
`librocksdb-dev` (apt) installs to /usr/lib/x86_64-linux-gnu (DNS2).
Both paths are in the default search path.
Verified locally on DNS2 with the exact CI cmake invocation + flags:
- build-fuzz-verify2: clean build, exit 0
- 20s fuzz pass: 1548 inputs, 2024 features covered, no crashes
Co-located with the libgflags fix on feat/script-fuzz-and-stress-tests
because both fixes are required for test-fuzz-smoke to turn green.
---------
Co-authored-by: Sami Ahmed <sami@sami-ahmed.net>
Replace undefined signed shifts in SPHlib SIMD FFT arithmetic with bounded multiplications, handle empty vectors in base64/base32/base58/hash/script paths, and skip the DoS_checkSig microbenchmark threshold under sanitizer instrumentation.
Sanitizer ctest is now green locally, so make the GitHub sanitizer job blocking again.
The hardened PowerShell retry loop from 2c2efd8 passed -ConnectionTimeout
and -OperationTimeout to Invoke-WebRequest. Those are PowerShell 7+ only;
GitHub Actions Windows runners ship PowerShell 5.1, which rejected them
with 'ParentContainsErrorRecordException / NamedParameterNotFound' on
the first iteration of the loop, and the catch block silently counted
the syntax error as a 'failed attempt' instead of a script bug.
Result on run #28689210122: both Windows jobs (build-windows-qt,
build-windows-daemon) failed at 'Download Tor' / 'Bundle Tor for daemon'
with exit code 1 before any HTTP traffic happened. macOS + Linux passed.
Fix:
* Drop -ConnectionTimeout and -OperationTimeout (PS7-only).
* Restructure the retry loop: explicit $downloaded flag, remove the
part-file on each attempt, throw explicitly at the end if all 3
attempts produced no usable file. The size check (>1MB) still
rejects 0-byte / truncated '200 OK' responses.
* Add a comment at the top of each step explaining the PS 5.1 limitation
so the next agent doesn't re-add the PS7 params.
The macOS build of e2cd0b6 (the NeedsBootstrap rocksdb/ fix) failed at
the 'Bundle Tor into app' step with bash exit code 6 after exactly 30s
of curl hanging against archive.torproject.org. All 4 Tor download
sites (Windows Qt, Windows daemon, Linux Qt .deb, Linux daemon .deb,
macOS Qt) used 'curl -sL' with no timeouts and no retries — a single
transient network drop from Azure westus to the Tor archive killed
the job.
Fix at all 4 sites:
* curl -fSL (HTTP error -> non-zero exit; fail loudly)
* --connect-timeout 15 / --max-time 120 (per-attempt bounds)
* --retry 3 --retry-delay 5 --retry-connrefused --retry-all-errors
(covers 5xx, DNS timeouts, and connection refused)
* 'set -euo pipefail' at script top so any failure aborts cleanly
* PowerShell variants get a manual retry loop with size check
(1MB minimum — a 0-byte '200 OK' response from a broken mirror
used to silently slip through)
Also bump CLIENT_VERSION_REVISION 1 -> 4 (v6.1.4) for the upcoming
release that will include e2cd0b6 (NeedsBootstrap rocksdb/ fix).
Release notes:
v6.1.4: Tor bundle download resilience (4 CI sites hardened)
+ e2cd0b6 (NeedsBootstrap rocksdb/ chain state detection). Supersedes
v6.1.3 only on CI reliability; no protocol/wallet/chain format changes.
The clang-format-diff and clang-tidy-diff jobs were hard-coded to
origin/${{ github.base_ref }}, which is empty under workflow_dispatch.
When the workflow was triggered manually (no PR context), both jobs
failed with 'Not a valid object name origin/' before doing any work.
Fallback path: when base_ref is empty, run clang-format/ clang-tidy
against initial commit..HEAD (i.e. the whole repo) so a manual dispatch
still produces a useful signal. Saves the diff to /tmp/changes.diff and
skips clang-tidy entirely if the diff turns out empty.
v6.1.3 distribute run (#28579791121) failed all 4 jobs (Homebrew, AUR,
Docker Hub, WinGet) because the build workflow took >12 minutes to
publish the GitHub release with binary assets, but the distribute
workflows only waited 10 minutes (30 iterations x 20s).
The race:
- Build workflow runs in parallel with Distribute workflow (no `needs:`)
- Distribute polls for the .deb/.dmg/.exe assets at the release URL
- Old 10-minute hard timeout was tuned for ~5 minute builds
- Modern builds (Windows, sanitizers, full Qt) routinely take 20-30 min
Bump all 5 wait loops (Docker Hub, AUR, Homebrew, Chocolatey, WinGet)
from 30 to 90 iterations, total 30 minutes, and update the error
messages to reflect the new timeout. Error messages also gained the
"after 30 minutes" suffix for consistency.
No change to the trigger conditions or job logic — only the timeout.
This is a workflow-only change; no source or CI matrix changes.
cpp20 modernization made SQLite the default wallet backend
(find_package(SQLite3 REQUIRED)). The windows-qt job got it
transitively via Qt5-base, but the daemon job had no such
dependency → CMake configure failure.
Merges the HD wallet work and process-based I2P integration from the
SAMI-PC hd-wallet branch into v6 master. Conflict resolution keeps
v6 embedded I2P (CI2PEmbedded) as primary, includes process-I2P
files for reference, preserves FastImportBlockFile() from hd-wallet,
and keeps v6 version numbers (6.0.0) and wAddressStack Qt layout.
Adds libi2pd static library build step and -DUSE_I2P_EMBEDDED=ON to
all 5 build jobs (Windows Qt, Windows daemon, Linux Qt, Linux daemon,
macOS). Previously I2P compiled as stubs — wallet shipped without
.b32.i2p address support. macOS uses HOMEBREW=1 for correct i2pd
Makefile include paths.
Same fix as build-all.yml: lint.yml's clang-tidy job also installed
librocksdb-dev from Ubuntu 22.04's apt (6.11.4), which CMakeLists.txt
now refuses to configure against. Drop the apt package, add the
shared scripts/ci/build-rocksdb.sh step.
PR #10 added a configure-time FATAL_ERROR for RocksDB < 7.4.0 because
the v5.9.24 daemon on DNS2 was built against librocksdb 6.11 and can't
read smsgDB SST files written by newer RocksDB (XXH3 per-block
checksum). The check worked — but it immediately failed CI, because
GitHub's ubuntu-22.04 runners also ship librocksdb-dev 6.11.4.
This is the same drift class the original patch was meant to prevent.
Fix: build RocksDB from source in CI, pinned to 8.9.1 (matching DNS2's
system version). Add scripts/ci/build-rocksdb.sh as a reusable helper
and call it from each of the four Linux jobs (test-linux-unit,
test-linux-sanitizers, build-linux-daemon, build-linux-qt). Drop
librocksdb-dev from the apt-get install (otherwise find_library would
pick up /usr/lib/librocksdb.so.6.11.4 first) and add libsnappy-dev /
libzstd-dev / liblz4-dev (compression libs RocksDB optionally links
against).
MacOS was already passing — Homebrew's rocksdb is current. Windows
was already passing — MSYS2's mingw-w64-rocksdb is at 9.x.
Also fix a cosmetic CMake bug: the version-detect function was setting
RocksDB_VERSION with PARENT_SCOPE only, so the 'Detected RocksDB
version from version.h:' message printed an empty value. Set the local
variable too so the STATUS message reflects the real value.
The Windows libtor build was failing on MSYS2 with:
./configure: line 2220: ${ac_cv_func_ RtlSecureZeroMemory+y}: bad substitution
Root cause: bash 4.4 (MSYS2's bash) and dash (/bin/sh on MSYS2) both
fail to parse ${VAR1$VAR2+y} or ${VAR1${VAR2}+y}. autoconf 2.69-2.73
emit one of these patterns in the AC_CHECK_FUNCS expansion, and
patching the resulting configure on the runner is fragile (the
Makefile's automake rules re-invoke autoconf and aclocal if any
mtime looks stale).
Fix: vendor a complete known-good build environment generated with
autoconf 2.71 on Linux. The vendored set:
src/tor/configure.vendored (37,966 lines, bash 4.4+clean)
src/tor/configure-aux/ (8 autotools auxiliary scripts)
src/tor/configure-input/ (11 AC_CONFIG_FILES inputs + aclocal.m4)
src/tor/regenerate-tor-configure.sh (one-shot regenerator with parse check)
src/tor/build-libtor.sh (uses vendored set when present)
build-libtor.sh now:
1. Copies configure.vendored + 8 aux files + 11 inputs into the
tor-src submodule directory.
2. Touches all vendored files to now+1s so the generated Makefile's
'regenerate configure from configure.ac' and 'regenerate
aclocal.m4 from m4/' rules see no work to do.
3. Runs configure directly (skips autoreconf entirely).
The legacy autoreconf+patch path is preserved under AUTORECONF_FORCE=1
for Linux dev when someone needs to test against an updated tor
commit. regenerate-tor-configure.sh handles regenerating the
vendored set from a fresh autoconf run.
Workflow:
build-all.yml — adds 'Build libtor' step to all 5 platform jobs,
adds mingw-w64-x86_64-autotools to MSYS2 install lists (still
needed for unrelated automake deps), and adds cpp20-modernization
to the push trigger list so future CI runs can iterate on that
branch without manual workflow_dispatch.
Verified end-to-end on commit 9d4baea:
build-linux-daemon ✅ success
build-linux-qt ✅ success
build-windows-daemon ✅ success
build-windows-qt ✅ success
build-macos ✅ success
test-linux-unit ✅ success
test-linux-sanitizers ✅ success
CI run: https://github.com/SamiAhmed7777/triangles_v5/actions/runs/28209275346
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.
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.)
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.
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)
* 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>
The secure-messaging store (smsgDB) used the LevelDB API directly. Mass-
mapped to the equivalent RocksDB types: leveldb::DB/Status/WriteBatch/
Iterator/Slice/ReadOptions/WriteOptions/WriteBatch::Handler -> rocksdb::*.
The RocksDB API surface for our usage is binary-compatible — pure namespace
substitution, no semantic changes. Consumers in rpcsmessage.cpp and
qt/messagemodel.cpp updated to match.
RocksDB now becomes a hard build dependency (was optional behind
BUILD_ROCKSDB). The chain-DB rocksdb backend is consequently always
available; -chaindb=leveldb remains the default until the Phase-4
LevelDB retirement. Removed the BUILD_ROCKSDB cmake option, the
#ifdef BUILD_ROCKSDB guards in txdb*, and the runtime error path
that triggered when the flag was off.
CI updated: librocksdb-dev (Ubuntu), mingw-w64-x86_64-rocksdb (MSYS2),
and rocksdb (Homebrew) added to all build jobs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Format and tidy enforce only on lines changed in PRs (diff-only via
git-clang-format and clang-tidy-diff.py) — existing files keep their
current style until edited. Mass reformat deferred; .git-blame-ignore-revs
stub is in place for whenever that happens.
Sanitizer lane builds with -fsanitize=address,undefined and runs the
unit suite. continue-on-error: true initially so we can triage findings
without blocking PRs. UB categories pervasive in the Hash9 C cascade
(alignment, signed-integer-overflow, vptr) are suppressed pending
file-by-file fixes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>