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>
Two coverage gaps closed in one commit because they were both
identified during the same test audit pass.
--- keystore_tests.cpp (NEW, 472 lines) ---
The keystore layer guards every spendable key in the wallet: a bug
here loses keys, accepts wrong keys, or breaks encryption round-trips.
The audit flagged it as security-critical with zero coverage.
27 cases:
- CBasicKeyStore: add/have/get roundtrips, missing-key negative cases,
pubkey derivation paths, secret compressed-flag preservation, GetKeys
enumeration + input-set clearing, CScript storage (BIP-0013) roundtrips
and idempotency.
- CCryptoKeyStore: state machine (initial state, LockKeyStore flip,
refuse-to-Lock-when-plaintext-keys-exist), encrypt/decrypt roundtrip
with the documented EncryptKeys -> Unlock sequence (not Unlock on a
plaintext store, which SetCrypted refuses), wrong-master rejection,
AddKey-when-locked refusal, AddKey-when-crypted-and-unlocked actually
encrypts, crypted-mode HaveKey/GetKeys/GetPubKey paths, edge cases
(empty store Unlock, double Unlock).
Uses TestableCryptoKeyStore (a unit-test-only subclass that widens the
protected Unlock/EncryptKeys access via using-declarations) so the test
can drive the protected paths without modifying production code.
--- staking_tests.cpp: GetWeight V5 soft-cap (8 cases) ---
The 2026-04-20 deploy added a 7-day soft cap to GetWeight that activates
ONLY when BOTH height >= FORK_HEIGHT_V5 (17651) AND nIntervalEnd >=
STAKE_AGE_SOFT_CAP_ACTIVATION (1776000000 = 2026-04-12 ~13:20 UTC). This
is the production code path for every stake on the live chain since the
deploy.
The existing staking_tests only covered the pre-V5 (nStakeMaxAge hard
cap) path, plus one negative test that confirmed the soft cap does NOT
apply pre-V5. The two production regimes -- V5+post-activation and
V5+pre-activation -- had no direct test coverage.
Adds 8 cases:
- V5+post-activation: cap at 7 days for stakes past the cap
- V5+post-activation: linear below the cap
- V5+post-activation: exactly at the cap (boundary)
- V5+post-activation: 1 second past the cap (boundary)
- V5+pre-activation: UNcapped (historical stakes preserve original rules)
- V5+activation-exact: >= semantics include the activation timestamp
- V5+high height (2.5M, like DNS2 live): cap unchanged by distance from fork
- V5+min-age floor: nStakeMinAge still returns 0 below floor
Uses RAII (BestChainGuard) to scope pindexBest swaps so a failed
assertion can't leave a stack pointer dangling in the global -- an
improvement over the manual save/restore pattern used in
consensus_safety_tests.
Verified: full test_triangles suite green (0 errors). Keystore 27/27,
staking 11/11 (3 original + 8 new), 21713+ assertions, ctest 4/4.
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>
PR #26 introduced an anonymous namespace at src/bootstrap.cpp:763 to hold
file-private helpers, but it never closed before the four public functions
declared in bootstrap.h:
- GetActiveTrustedSnapshotPublisher
- LoadTrustedSnapshotPublisher
- SetTrustedSnapshotPublisher
- UnsetTrustedSnapshotPublisher
With these inside the anonymous namespace, the compiler mangles them as
Bootstrap::(anonymous_namespace)::*, while the header declares them as
plain Bootstrap::*. Result: any caller (rpcblockchain.cpp, init.cpp)
fails to link with 'undefined reference to
Bootstrap::GetActiveTrustedSnapshotPublisher'. PR #27 inherited a
master that didn't build and CI was red across all jobs.
Fix: close the anonymous namespace immediately before the public
functions, then re-open it afterwards for the remaining file-private
helpers (IsTrustedSnapshotSigner / VerifySignedMessage /
ExtractJsonString).
Verified:
- nm confirms Bootstrap::GetActiveTrustedSnapshotPublisher is now T
(external linkage) on bootstrap.cpp.o
- ninja builds trianglesd and test_snapshotnet cleanly
- rpcblockchain.cpp.o compiles (the consumer that was failing)
- ctest: 4/4 suites pass (triangles_unit_tests,
chaindb_equivalence_tests, snapshotnet_tests, chaindb_runtime_tests)
- existing anonymous namespace at lines 455-470 unchanged
This should unblock PR #27 (feat/script-fuzz-and-stress-tests) CI.
Design A: single-slot runtime override via RPC. The previous publisher
is dropped atomically on every set. The built-in fallback list
(TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX, Sami's legacy key) is always
consulted if no runtime override is set, so a fresh daemon still
verifies old snapshots without operator intervention.
New RPCs:
- settrustedv2snapshotpublisher <address>
- gettrustedv2snapshotpublisher
- unsettrustedv2snapshotpublisher
Persistence: <datadir>/snapshot-publisher.json (plain JSON).
Loaded at startup in init.cpp before any snapshot verification.
Files:
src/bootstrap.cpp (+116 / -8) Replace hardcoded list with single-slot + fallback
src/bootstrap.h (+21) Declare new Bootstrap:: functions
src/init.cpp (+3) LoadTrustedSnapshotPublisher() at startup
src/rpcblockchain.cpp (+89) Three new RPC function bodies
src/rpcblockchain.cpp (+1) #include "bootstrap.h"
src/trianglesrpc.cpp (+3) Register three new commands
src/trianglesrpc.h (+3) extern declarations
README.md (+30) New 'Trusted Snapshot Publisher' sections
TRIANGLES-RPC-COMMANDS.md (+3) Three new rows in Blockchain table
docs/snapshot-publisher.md (new, +240) Full operator handoff guide
Co-authored-by: Krystie <krystie@openclaw.local>
int64_t is ambiguous with QVariant's overload set (int / uint /
qlonglong / qulonglong / bool / float / double). Wrap in
QVariant::fromValue<qlonglong> to disambiguate. Fixes the
build-linux-qt failure on the rebased v6.1.7 PR.
Three user-visible changes since v6.1.6:
1. Total label font-weight 75 -> 900 (full bold). v6.1.6 used
'font: 12pt bold' which Qt maps to weight 75, indistinguishable
from the other bold balance labels. Now 'font: 900 12pt'.
2. Transactions amount column Confirming tier color changed from
#C5EBC9 (pale mint) to #4A8C5E (mid green). The pale mint was too
close to the bright #7CDB8A Confirmed green on the dark background
and read as the same color to the user. Mid green sits clearly
between grey (#61280E Unconfirmed) and bright green (#7CDB8A
Confirmed) so the three tiers are visually distinct.
3. Both amount paint sites (Transactions tab + Overview recent-5)
now read confirmation depth via a new DepthRole on
TransactionTableModel instead of going through the
TransactionStatus enum. The rule fires on every block increment,
not only on enum state transitions.
Internal: added DepthRole to TransactionTableModel::ColumnRole
enum. transactiontablemodel.cpp::data() handles the new role.
overviewpage.cpp::TxViewDelegate::paint() queries DepthRole.
Packaging metadata, CHANGELOG, RPM %changelog updated to 6.1.7.
Bumps clientversion + all packaging metadata from 6.1.6 to 6.1.7.
Includes only the v6.1.6 polish changes plus the Total-bold fix from
PR #24 (font-weight bumped from 75 to 900). CHANGELOG entry added.
Not included in v6.1.7 (will be addressed in v6.1.8 once we have
repro data from a user observing the rule on real stakes):
- Any widening of the 3-tier amount-color Confirming tier
- Any 'use depth directly instead of status enum' rewrite
- Any changes to the dataChanged() signaling path
The Total label will now render at full bold weight 900 instead of
medium-bold 75, which should make it visibly heavier than the
Spendable / Stake / Unconfirmed rows on the Overview panel.
The v6.1.6 conditional Total color shipped with font: 12pt bold,
which Qt interprets as font-weight 75 (medium-bold). That's the
codebase's existing convention for setStyleSheet bold labels
(trianglesgui.cpp uses 'font-weight: bold' for the Tor/I2P status
indicators) but it's not visually distinct against the label font.
Bump to font: 900 12pt (font-weight 900, full bold) so the Total
actually stands out as the headline number on the Overview panel.
No other behavior changed. Just font weight.
Bumps clientversion from 6.1.5 to 6.1.6 and updates all packaging
metadata (deb, rpm, docker, snap, flatpak, winget, scoop, appimage,
root Dockerfile) to match. Adds a v6.1.6 section to CHANGELOG.md
documenting the conditional Overview Total label and the 3-tier
amount-column color rule that landed in this release. Restores the
historic v6.1.5 entry in triangles.spec's %changelog after the bulk
sed bumped it incorrectly.
The 3-tier amount color rule references TransactionStatus::Confirming
directly. transactiontablemodel.h only forward-declares TransactionStatus
(it does not include transactionrecord.h), so the inner enum value
'Confirming' was not visible in the overviewpage.cpp translation unit.
This manifested as a build failure on every Qt build (linux-qt, macos,
windows-qt) on the rebased PR #22. Daemon builds were unaffected because
they don't compile overviewpage.cpp.
Fix: include transactionrecord.h in overviewpage.cpp so the
TransactionStatus enum values are in scope.
Overview Total:
- Was static green in stylesheet (failed to cascade on some Qt builds)
- Now set programmatically in setBalance(): green when total > 0,
red when empty. Stylesheet rule for #labelTotal removed; C++ owns
the color so the rule can react to the balance value.
Transaction amounts (both paint sites, Overview recent-5 + Transactions tab):
- 3-tier rule using existing TransactionStatus enum:
0 confirms (Unconfirmed) -> COLOR_UNCONFIRMED grey (#61280E)
1..3 confs (Confirming) -> COLOR_CONFIRMING pale (#C5EBC9)
4+ confs (Confirmed) -> COLOR_POSITIVE bright (#7CDB8A)
Conflicted -> COLOR_UNCONFIRMED grey
Immature -> olive via .ui (unchanged)
- Negative amounts (spent) stay red across all tiers
- Now matches the icon column's existing state distinction
(transaction_0 / transaction_1..3 / transaction_confirmed)
Files touched:
src/qt/guiconstants.h new COLOR_CONFIRMING constant
src/qt/overviewpage.cpp Total rule + 3-tier amount rule
src/qt/transactiontablemodel.cpp 3-tier amount rule in ForegroundRole
src/qt/forms/overviewpage.ui removed static #labelTotal rule
Replaces the previous 3-color rule with a finer-grained one that
matches how the rest of the codebase already classifies transaction
state via TransactionStatus enum:
Status | Amount color | Hex
---------------------+--------------------------+--------
Unconfirmed (0 conf) | COLOR_UNCONFIRMED grey | #61280E
Confirming (1..3) | COLOR_CONFIRMING pale | #C5EBC9
Confirmed (4+) | COLOR_POSITIVE bright | #7CDB8A
Conflicted | COLOR_UNCONFIRMED grey | #61280E
Immature | (olive, via .ui, unchanged)
Negative any tier | COLOR_NEGATIVE red | #FF0000
RecommendedNumConfirmations = 4 (existing constant in transactionrecord.h),
so the Confirming tier covers depths 1, 2, 3 and the Confirmed tier
covers 4+. The codebase already uses this same state distinction for
the icon column (transaction_0 / transaction_1..3 / transaction_confirmed),
so the amount column now matches the icon's signal.
Both paint sites updated:
- src/qt/transactiontablemodel.cpp ForegroundRole
- src/qt/overviewpage.cpp recent-5 painter
New constant in guiconstants.h:
COLOR_CONFIRMING QColor(197, 235, 201) — soft mint, deliberately
pale so it reads as 'partial' vs the saturated #7CDB8A 'final' green.
Split the grouped ID selector so #labelTotal gets its own rule with
explicit font (12pt bold) inline. Hardens against Qt cascading
edge cases where a peer-selector group could be parsed-out by
an older Qt build or silently dropped if one ID doesn't match.
Fixes: 'Total on Overview always renders red' UI bug
The RPM spec had no %changelog section, so 'rpm -q --changelog triangles'
returned nothing and downstream tooling (dnf/yum repoclosure, COPR
audit) flagged the package as low-quality. Add entries for the 6.x
release line (6.1.5, 6.1.4, 6.1.3, 6.1.1, 6.1.0) and 5.3.7.
Skipped v6.1.2: that release was yanked (5c312bb published 2026-07-01,
superseded by 6.1.3). Including it would mislead anyone searching the
changelog for the actual v3-snapshot fix.
Dates match git tag dates. Maintainer identity uses the project email
sami@cryptographic-triangles.org (matches other release metadata).
doc/release-process.md says the artifact-signing public key MUST be
committed to the repo at release-pubkey.asc so verifiers can confirm
signatures. This was a documented gap that was never closed.
The key in question is the Krystie Triangles Release key (fingerprint
523A 8183 3EB7 2015 73E1 EFE1 DCF2 5799 6810 7984), which signs the
release artifacts in CI. v6.1.5 (and v6.1.4) artifacts were already
signed by this key; verifiers can now confirm against the key in
this file.
Verifying a v6.1.5 artifact:
gpg --import release-pubkey.asc
gpg --verify SHA256SUMS.asc
The maintainer's tag-signing key (Sami personal, 0x0BF7F8872FE0E859)
is NOT published here on purpose: that key is exported only to
release-pubkey.asc backup files (Sami's Dropbox / local backups).
The doc explains the two-key model.
The v6.x release line was missing from
packaging/appstream/org.cryptographic_triangles.TrianglesQt.metainfo.xml,
which means software centers (GNOME Software, KDE Discover, elementary
AppCenter, Flatpak, etc.) show the wallet as stuck at v5.3.7. bump-version.sh
flags this file as a manual follow-up; this commit closes that gap.
Skipped v6.1.2: that release was yanked (5c312bb published 2026-07-01,
superseded by 6.1.3) and the v6.1.3 changelog already documents the
replacement. Listing a yanked release would mislead users searching
for it.
The script previously assumed libtor.a and libi2pd*.a were already
present, but on a fresh checkout they only exist after running
src/tor/build-libtor.sh and src/i2p/build-libi2pd.sh. CI does this
in build-all.yml but local verification didn't, which bit me during
the v6.1.5 release.
Detect missing static libs and invoke the build scripts (passing
/usr paths for native Linux, matching what CI does). On a fresh
checkout this adds ~8 min to first-run verification; subsequent
runs skip the build step.
Logs go to /tmp/triangles-build-lib{tor,i2pd}.log for debugging.
Exit code 5 distinguishes build-prep failures from cmake/build
failures (3) and binary-compare failures (1/4).
Claude's 2026-07-07 audit of 2a4da33 (PoS reward rework) and 239cf61
(sigcache fix) concluded:
- 2a4da33 must stay reverted: chain-split risk, motivation gone
(a78a420 already relaxed the only test that cared), and the new
formula is worse than the old (drops fractional coin-age, int64
overflow risk on large coin-age). If exact proportionality is
ever wanted, it requires a height-gated hard fork.
- 239cf61 is safe to re-land: pure performance fix, no consensus
change, SHA256-collision false-positive risk is cryptographically
infeasible. Re-landed in PR #21 / fix/sigcache-false-positives
as a 6.1.6 candidate.
T024 in V6_TASKS.md records the rejection and the hard-fork
prerequisite for any future re-attempt.
The 2026-07-04 sigcache fix (239cf61, originally reverted, re-landed
here) changed the cache entry from a 64-bit XOR-mix to a uint256
SHA256(sighash || sig || pubkey). Default capacity is 200,000
entries, so peak memory grew from ~1.6 MB to ~6.4 MB. The stale
comment claimed 8 bytes per entry; correct that.
No code change — comment only. Confirmed via Claude's 2026-07-07
review of the reverted commits that re-landing 239cf61 is safe
(performance fix, no consensus change, SHA256 collision risk is
cryptographically infeasible).
Two stacked bugs in CSignatureCache:
1. Set() keyed on vchSig (with trailing hashtype byte) while Get() keyed
on vchSigCopy (without), so the cache never hit: a silent no-op.
(Found in prior audit session.)
2. Once (1) was fixed, the cache produced FALSE POSITIVES: the 64-bit
XOR-mixed key included the pubkey LENGTH but never the pubkey BYTES.
All compressed pubkeys are 33 bytes, so a signature validated once
hit the cache when re-checked against ANY other pubkey for the same
sighash — CheckSig returned true without verifying. A 2-of-3
CHECKMULTISIG could be satisfied by one valid signature duplicated.
This also masqueraded as first-match-wins multisig reordering in
multisig_tests/script_tests; those tests now pass with their original
strict assertions.
Cache entries are now the full SHA256 over (sighash || sig || pubkey),
matching upstream Bitcoin Core; false positives are cryptographically
infeasible.
36 commits since v6.1.4 (2026-07-04). User-facing:
- UI: olive-green for unconfirmed/immature stakes
- Wallet: close-hang on Windows from detached Tor/I2P threads fixed
- Consensus: live PoS checks during stale-tip IBD
Maintainer-visible:
- CHANGELOG.md added at the repo root
- doc/release-process.md corrected to match the actual signing keys
(RSA-4096 Krystie release key + Sami personal tag-signing key)
Pending and immature balance labels render in olive (#A8B847),
visually distinct from confirmed positive balances (#7CDB8A) while
still reading as 'incoming' rather than 'outgoing' (red).
* fix(wallet): prevent exit-hang on Windows from detached Tor/I2P threads
Embedded Tor and embedded I2P each ran on a background std::thread that was
.detach()'d at startup. The teardown paths (CTorEmbedded::Stop,
CI2PEmbedded::Stop) only flipped a running-flag — they did not signal the
thread to exit, and on Windows there is no signal mechanism in tor_api 0.4.x.
Result on Windows: when the user closed the wallet, Shutdown() completed its
bookkeeping and main() returned 0, but the process could not exit because the
detached thread was still in the Tor event loop / i2pd io_context. End Task
(TerminateProcess) was the only escape; the GUI appeared completely stuck.
Fixes:
- tor_embedded.h/.cpp: keep the Tor thread handle; Stop() now raise(SIGTERM)
on Linux, then joins the thread with a 5s timeout, then TerminateThread
(Win) / pthread_cancel + pthread_join (Linux) as a last resort.
- i2p_embedded.h/.cpp: same pattern — capture the bootstrap thread and join
it in Stop() with a 5s timeout fallback.
- init.cpp Shutdown(): spawn a 30s watchdog thread that calls ExitProcess(1)
if the graceful teardown takes too long. Belt-and-suspenders against any
future deadlock in the exit path.
- trianglesgui.cpp closeEvent(): second close attempt while the first
exit is still running immediately calls ExitProcess(2) / _exit(2).
User escape hatch when the graceful exit hangs.
All non-consensus (threading/process lifecycle only). Build via CI; not local.
Notes: notes/wallet-close-hang-fix-2026-07-07.md
* fix(i2p): drop leftover .detach() that broke build (lambda now joinable)
* fix(i2p): clean up after .detach() removal (trailing comment, blank line)
* fix(tor): MINGW std::thread is pthread-based, use pthread_cancel/join on MINGW
MINGW std::thread::native_handle_type is unsigned long long (pthread_t
emulation), not HANDLE. Mixing pthread handles with Win32
WaitForSingleObject/TerminateThread fails to compile on MINGW with
'invalid conversion' errors.
Use the same pthread_cancel/pthread_join path on Linux and MINGW; keep
TerminateThread only for MSVC builds where native_handle() returns a
real Win32 HANDLE.
---------
Co-authored-by: krystie <krystie>
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.
Adds the infrastructure for verifiable Triangles releases:
- Reproducible builds (default-on): -ffile-prefix-map strips absolute
source paths from binaries; SOURCE_DATE_EPOCH pinned to commit
timestamp if env var not set. Two builds of the same commit with the
same flags now produce byte-identical binaries.
- scripts/verify-reproducible-build.sh: builds the daemon twice into
separate build dirs and compares SHA256. Pass/fail printed clearly.
- scripts/sign-release.sh: generates SHA256SUMS, writes detached .asc
signatures over each release artifact and over SHA256SUMS itself.
Supports --verify for independent third-party verification.
- release-process.md: canonical release pipeline documentation --
reproducibility properties, signing-key setup, distribution
requirements, failure-mode recovery, and the release checklist.
- scripts/README.md: updated to catalog the full scripts/ directory
(was previously scoped only to bump-version.sh).
Verified end-to-end on this branch:
- scripts/verify-reproducible-build.sh: exit 0, both builds SHA256
7a86d9659b7150f69dc53eb31cc4c7eb8df296b55fa889af5c5a1b310223c894.
- scripts/sign-release.sh: signs Release-built artifact, --verify
returns exit 0 (all sigs + checksums valid).
- ctest: 4/4 suites still pass with the new compile flags.
- Tamper test: modifying an artifact after signing causes --verify
to fail with '1 checksum(s) FAILED' (exit 1).
Existing signing key in the local keyring is used:
523A81833EB7201573E1EFE1DCF2579968107984
(Krystie Triangles Release <krystie-triangles-release@dns2.sami.tailnet>)
CI integration (separate PR): add a 'sign' job to build-all.yml that
imports GPG_PRIVATE_KEY from secrets and runs scripts/sign-release.sh
against the assembled release directory. Documented in release-process.md.
Documents:
- V5 soft-cap test coverage shipped on audit/kernel-coverage (ab0f4b4)
- PR #14 CI status: test-linux-unit PASS, sanitizer FAIL pre-existing
(simd.c:265 UBSan, separate workstream)
- Outstanding work prioritized for future sessions
- PR #14 is ready to merge
Documents:
- Hermes's 2026-07-04 handoff letter had a stale 'blocked on W2' framing;
W2/H4/W1 were already committed as 6cadf7f on 2026-07-02.
- This session's DoS_checkSig timing fix (commit b79e2b8): replaced the
nonsensical nManyValidate < nOneValidate comparison with a stable
per-verify bound (min of 3 trials after warmup, threshold 600ms
calibrated to ~1.6x observed p100 on DNS2).
- PR #14 CI status: 9 jobs in progress as of session end.
The previous timing assertion (nManyValidate < nOneValidate) was never
meaningful: the loops did different op counts (100 signs vs 500 verifies)
and the signature cache is intentionally a no-op on master, so cached-vs-
uncached verify cost is identical. The downgrade to BOOST_WARN_MESSAGE
that was on the branch fires every run.
Replace it with a real regression check: take the min of 3 timed batches
of 500 verifies after a warm-up pass, then assert the min is below an
empirically-calibrated threshold (600ms on this DNS2 dev box; real perf
~380ms in debug builds).
This catches genuine verify-path regressions (accidental O(n) cache key,
double-verify, hooking up OpenSSL instead of libsecp256k1) without
coupling to cache speedup that the on-chain code path explicitly avoids.
227/227 test cases pass, 21597/21597 assertions, 0 failures.
The hardcoded mapCheckpoints in src/checkpoints.cpp only covers heights
0..~17650 (the v5 hard fork pin). Everything from 17651 to current tip
(~2.2M blocks at the time of writing) runs full sigops/script/UTXO
validation in ConnectBlock. This is the actual sync bottleneck for new
nodes — days instead of hours.
The existing optimization (line 2179) skips input validation for blocks
at or below the last hardcoded checkpoint. This commit extends that
optimization with a ROLLING threshold: blocks at or below
nAssumeValidThreshold also take the fast path. The threshold advances
after each successful SetBestChain by ASSUME_VALID_BUFFER (100) blocks,
so the last 100 blocks are always fully validated — reorgs are caught
immediately.
Trust model:
- Hardcoded checkpoints: trusted at build time, source code is public.
Reproducible builds can verify.
- Rolling threshold: trusted because we validated it ourselves last
time. Same security guarantee as the static checkpoint, just newer.
- No master key, no centralized checkpoint authority, no new trust
anchor introduced. The chain itself is the proof.
Decentralization preserved: every node independently advances its own
threshold based on its own successful validation history. No coordination
required. A node that started from a different bootstrap will reach the
same threshold eventually.
Safety properties:
- ASSUME_VALID_BUFFER = 100 (matches MAX_REORG_DEPTH). A reorg that
rewrites within the buffer triggers full validation and rejection.
- Threshold only advances when NOT in IBD — we don\'t lock in a wrong
chain during initial sync.
- Threshold never decreases — reorgs can\'t accidentally lower the
fast-path boundary.
TODO before production deploy (called out in code comments):
- Persist nAssumeValidThreshold to wallet DB on shutdown so restarts
don\'t reset to 0 and re-validate 2.2M blocks.
- Add RPC: getassumevalidthreshold so operators can monitor.
crypter.cpp had zero tests despite guarding every encrypted wallet. Add
8 cases: passphrase round-trip for both KDFs (sha512 method 0 and scrypt
method 1), wrong-passphrase rejection, salt-affects-key, KDF determinism,
bad-parameter rejection (zero rounds / short salt / encrypt-before-key),
the EncryptSecret/DecryptSecret private-key path with a uint256 IV, and
ciphertext-tamper rejection. Round-trip/negative style, no brittle hard-coded
ciphertext. No implementation change (crypter.cpp is correct).
Note captured in the test: the wallet uses a uint256 as the AES IV but
AES-256-CBC consumes only the first 16 (little-endian) memory bytes -- a
subtlety worth remembering for anyone touching the key-encryption path.
Three coupled fixes to the test harness (no consensus/runtime code touched):
1. Root CMakeLists never called enable_testing(), so the top-level
build/CTestTestfile.cmake was never generated and "cd build && ctest"
(exactly what CI runs) discovered ZERO tests. The whole unit suite was
silently not gating CI; only the explicitly-invoked equivalence binary
ran. Add enable_testing() at the root so ctest finds all four test
executables.
2. chaindb_runtime_tests.cpp and snapshotnet_tests.cpp were compiled BOTH
into their own standalone executables AND into test_triangles via the
test/*.cpp glob. Each #defines its own BOOST_TEST_MODULE and redefines
the wallet/UI globals; the link only survived via
-Wl,--allow-multiple-definition, which silently drops duplicate module
and global symbols and can run those suites under the wrong fixture.
Exclude both from the glob (they already have dedicated add_executable +
add_test); nothing is lost and isolation is restored.
3. test_triangles TestingSetup opened the PRODUCTION chain DB at the default
datadir, so ctest failed (DB lock) on any host running a live daemon and
risked touching real chain state. Point -datadir at a fresh temp dir in
the fixture (mirrors the standalone DataDirSetup); cleaned up on teardown.
After: ctest -N lists 4 tests; ctest runs 100% green even with a live
trianglesd holding the default datadir.