473 Commits

Author SHA1 Message Date
Sami Ahmed 021d4bf093 test(consensus): make rogue-guard scan walk src/ via std::filesystem
Round-4 review of e1ff615 caught that hardened_checkpoint_no_rogue_guard_in_other_files
used a hand-curated expected_clean_files list with 3 nonexistent filenames and
silently skipped them, leaving ~80 production .cpp/.h files (including
src/bootstrap.cpp, src/syncmanager.cpp, src/checkpointpublisher.cpp) unscanned.
The test's preamble claimed it walked src/; the implementation didn't.

Replace the hand-curated list with a recursive walk via std::filesystem
(C++20, already in use). excluded_dirs: src/test, src/qt, src/tor, src/i2p,
src/leveldb. Allow-list (intentional references): src/main.{cpp,h},
src/init.cpp, src/checkpoints.{cpp,h}. Sanity assertion: the walk must find
at least one file, so a misconfigured scan can't silently pass.

Verified: temporarily injecting 'pindexLastHardenedCheckpoint' into
src/bootstrap.cpp fails the test with the correct file name; restoring the
file passes it. No false positives. 280/280 tests green.
2026-07-16 23:04:05 -07:00
Sami Ahmed e1ff615233 test(consensus): harden off-by-one + cross-file rogue-guard detection
Adversarial Codex review of 6116cff (round 3) flagged two test-quality
observations that don't affect correctness of the current SHA but could
let future regressions slip through:

1. The source-grep test reorg_guard_fails_closed_when_checkpoint_pointer_null
   didn't pin the boundary operator. A refactor from '<=' to '<' would
   still pass the existing assertions but weaken the guard. New test
   reorg_guard_offbyone_hardening pins the operator as '<=' (and rejects
   '<' and '>='), pins the runtime return value of
   Checkpoints::GetLastCheckpointHeight() against the compiled map
   (currently 2214400), and pins the reject-message wording.

2. The same grep test only scanned src/main.cpp. A consensus guard added
   to a different production file (e.g. src/miner.cpp) would silently
   bypass it. New test hardened_checkpoint_no_rogue_guard_in_other_files
   enumerates the production files we expect to be free of any reference
   to pindexLastHardenedCheckpoint and asserts they remain so.

Also polishes the startup-init comment block in src/init.cpp:1381 with
explicit cross-references to Reorganize()'s bootstrap-time fallback and
clarifies that getheaders is serving-side only (not a consensus guard).

Build: clean. Tests: 280/280 pass (was 278).
2026-07-16 22:56:34 -07:00
Sami Ahmed 6116cff52b fix(consensus): fail-closed reorg guard when startup checkpoint pointer is null
Adversarial Codex review of 935d1d5 flagged that Reorganize()'s below-checkpoint
guard short-circuited on 'pindexLastHardenedCheckpoint == nullptr'. That state
arises during early IBD, reindex, and bootstrap before the checkpoint block has
been downloaded into mapBlockIndex — exactly when an attacker peer would feed a
deep fork. Without the pointer, the guard silently fell through.

Fix: add Checkpoints::GetLastCheckpointHeight(), which reads the compiled
mapCheckpoints directly (independent of mapBlockIndex). Reorganize() now
resolves nHardenedCheckpointHeight from the pointer when available, falling
back to the compiled height otherwise. Same floor, just two paths.

Tests: 278/278 pass; added reorg_guard_fails_closed_when_checkpoint_pointer_null
with four invariant checks (helper existence, fallback assignment, guard
pattern, absence of the old short-circuit pattern).
2026-07-16 22:20:42 -07:00
Krystie 935d1d527c fix(consensus): remove local-finality, fix getheaders fork recovery
fix/consensus-convergence — the rules around reorg finality and the
getheaders fork-peer handler previously used locally advanced state
that prevented two honest nodes from converging after extended
disconnection. This commit removes the local-finality rules and
restores convergence above the last globally shared hardened
checkpoint.

Reorganize() now:
- Rejects reorgs whose fork point is at or below the compiled
  hardened checkpoint (sourced from Checkpoints::GetLastCheckpoint
  at startup, never advanced at runtime).
- Above the checkpoint: greatest cumulative chain trust wins. No
  depth cap, no local finality, no 10% trust hysteresis.

pindexFinalized is renamed to pindexLastHardenedCheckpoint to make
clear that the variable now refers to the compiled checkpoint anchor,
not a locally advanced finality depth. Its initialization in init.cpp
runs once at startup; no runtime advancement.

The getheaders handler now serves canonical history based on what the
peer actually knows:
- If the peer's locator contains the hardened checkpoint, serve
  headers from the checkpoint forward.
- Otherwise, serve from the last common ancestor (falling back to
  genesis if no overlap exists). This lets a forked peer recover
  instead of being handed a header whose parent it doesn't have.

CBlockLocator gains two small public accessors (Has, FindCommonAncestorInMainChain)
so the recovery code doesn't have to reach into protected state.

Staking safety gate is now continuous in StakeMiner (main.cpp's
IsStakingSafe runs every iteration). Removed the once-only fTryToSync
flag whose reset-after-first-use made the strong peer-count / IBD
check ineffective after a network outage mid-staking. The gate refuses
to stake when IBD is active, fewer than 2 handshaken peers exist, our
height is behind the peer median, or a peer reports a tip >=2 blocks
ahead of ours (possible competing fork signal).

Tests:
- consensus_safety_tests.cpp: 6 new tests pinning the convergence
  rule's structure against src/main.cpp and src/init.cpp. Replaces the
  old max_reorg_depth_enforced test (which pinned the now-removed
  local-finality constant).
- staking_tests.cpp: 3 new tests pinning the continuous gate's
  behavior and the absence of fTryToSync from runtime code.

All 277 unit-test cases (21,752 assertions) pass locally. The Qt GUI
was not rebuilt; the daemon (trianglesd), CLI (triangles-cli), and
test binary (test_triangles) all link and execute.

Reviewed-against: pre-commit HEAD
No push to master performed per standing rule.
2026-07-16 21:47:21 -07:00
Sami Ahmed c68a8cb47c fix(ibd): allow getblocks/getheaders on OneShot peers during IBD
The version-handler fShouldAsk gate at main.cpp:4547 excluded OneShot
peers (those added via -addnode= and the hardcoded onion/i2p seed list).
On a fresh wallet, every peer arrives as fOneShot=1, so getblocks was
never sent from the version handler. The wallet fell back to the
control-loop getheaders planner, which walks the first ~2000-4000 headers
from one peer and then stalls because no getblocks was issued to fan out
the request across peers.

Empirically verified against SAMI-PC debug.log (v6.1.7.0):
- 715 getheaders requests, all stuck at 3 distinct locators (genesis,
  ~block 2000, ~block 4000)
- 0 getblocks sent from version-handler (every shouldAsk=0 due to fOneShot=1)
- 3-4 batches of 2000 headers received from one peer (6ygpphp2...onion)
- Wallet stuck at 4000/2,221,278 blocks

With this fix, the version handler issues getblocks to every fOneShot
peer during IBD, allowing multi-peer concurrent sync from genesis.

Verified on DNS2 with master binary against fresh datadir:
- 11 shouldAsk=1 events (was 0)
- 11 'sent getblocks+getheaders from height 0' events (was 0)
- headers accepted: 3 batches (6000+) (was 0)
2026-07-12 04:02:28 -07:00
SamiAhmed7777 db467925ca test: keystore + V5 soft-cap coverage (#29)
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>
2026-07-11 16:02:39 -07:00
SamiAhmed7777 9a50ab3b2e Expand script.cpp test coverage: libFuzzer harness + EvalScript stress tests + UBSan fix (#27)
* 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>
2026-07-11 16:02:35 -07:00
Sami Ahmed 898292ff2b fix(bootstrap): restore Bootstrap:: linkage for trusted-publisher API
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.
2026-07-11 01:06:26 -07:00
SamiAhmed7777 8598cfa781 feat(bootstrap): RPC-driven trusted snapshot publisher rotation (v6.1.8) (#26)
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>
2026-07-10 17:29:15 -07:00
Sami Ahmed db67ccfa28 fix(qt): explicit QVariant::fromValue<qlonglong> for DepthRole
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.
2026-07-08 18:18:52 -07:00
Sami Ahmed 5d0e14370d release: v6.1.7 (bold Total + distinct Confirming color)
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.
2026-07-08 18:00:35 -07:00
Sami Ahmed 56d999f6f1 release: v6.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.
2026-07-08 17:57:44 -07:00
Sami Ahmed 290970097e ui: force Total label to true bold (font-weight: 900)
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.
2026-07-08 16:54:55 -07:00
Sami Ahmed d82a74eefc release: v6.1.6
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.
2026-07-08 14:02:57 -07:00
Sami Ahmed 5b7db2ccd3 fix(ui): include transactionrecord.h in overviewpage.cpp
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.
2026-07-08 13:42:31 -07:00
Sami Ahmed b67d17b2a5 ui: 3-tier Amount color + conditional Total color (v6.1.6 polish)
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
2026-07-08 13:13:56 -07:00
Sami Ahmed 6ba38e6f7a ui: 3-tier Amount color (grey / light green / bright green) by confirmation depth
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.
2026-07-08 13:13:56 -07:00
Sami Ahmed 1cb42e0b02 ui: force labelTotal to render green on overview
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
2026-07-08 13:13:56 -07:00
Sami Ahmed fa683c2655 fix(sigcache): update comment for new entry size
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).
2026-07-07 22:33:26 -07:00
Krystie 37142195b9 script: fix signature cache false positives (security)
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.
2026-07-07 22:33:08 -07:00
Sami Ahmed 148cfd63c7 release: v6.1.5
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)
2026-07-07 21:10:41 -07:00
Sami Ahmed fb5db71b53 ui: olive-green for unconfirmed/immature stakes
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).
2026-07-07 21:09:44 -07:00
SamiAhmed7777 ff90824247 fix(wallet): prevent exit-hang on Windows from detached Tor/I2P threads (#20)
* 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>
2026-07-07 18:56:21 -07:00
SamiAhmed7777 3143a03af6 ui: recolor overview — orange→yellow icons, green positive balances/txns, red-off I2P/Tor (#19)
- Palette orange (242,101,34) → light yellow (255,224,102) in overviewpage.ui
- Orange derivative shades (Light/Midlight/Mid/Dark/AlternateBase) → yellow tints
- Spendable/Total/Stake/Unconfirmed/Immature balance labels: green (#7CDB8A)
- Transaction list: positive amounts green, negative red (was palette-text / red)
- Overview recent-txns delegate: positive amounts green via COLOR_POSITIVE
- I2P/Tor/V3 status icons: green when active, red (#e32105) when off
- Keep labelWalletStatus 'out of sync' red, frame borders red (#e32105)

Co-authored-by: krystie <krystie@local>
2026-07-07 18:07:19 -07:00
Krystie c06046b604 consensus: keep live PoS checks during stale-tip IBD 2026-07-07 15:59:29 -07:00
Krystie b9d06d5f77 ci: fix sanitizer failures
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.
2026-07-07 14:42:30 -07:00
SamiAhmed7777 a5e299cf2c Merge pull request #15 from SamiAhmed7777/audit/sync-fast-assumevalid
main: extend assumeValid fast path past hardcoded checkpoints
2026-07-07 13:32:43 -07:00
SamiAhmed7777 6e53f6f941 Merge pull request #14 from SamiAhmed7777/audit/sigcache-walletdb-test-fixes
audit: test repair + walletdb SQLite cursor fix + consensus safety suite
2026-07-07 13:32:19 -07:00
Krystie b79e2b8215 test: replace DoS_checkSig cache-timing WARN with a stable per-verify bound
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.
2026-07-06 22:56:24 -07:00
Hermes 223c50f92f main: extend assumeValid fast path past hardcoded checkpoints
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.
2026-07-04 21:57:52 -07:00
Krystie 43eab5f8cd test: add wallet-encryption (CCrypter) coverage
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.
2026-07-04 19:35:00 -07:00
Krystie f0889d9b70 test/build: make ctest actually run the unit suites
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.
2026-07-04 16:25:26 -07:00
Krystie 30d9e9296d test: soften DoS_checkSig sig-cache timing to a WARN
With the signature-cache optimization intentionally left disabled (no
consensus-critical changes), cached and uncached verification cost the same,
so the nManyValidate < nOneValidate timing relation is not guaranteed. This
is a machine-dependent performance heuristic, not a correctness check, so
downgrade it from a hard CHECK to a WARN. CheckSig correctness is covered by
the multisig and script suites.
2026-07-04 15:15:12 -07:00
Krystie 36d5f2928f Revert "script: fix signature cache false positives (security)"
This reverts commit 239cf61795.
2026-07-04 15:10:07 -07:00
Krystie a78a420d76 test: tolerate 1-unit truncation rounding in PoS reward proportionality
After reverting the consensus-affecting PoS reward rework, the original
truncating formula (nCoinAge * rate / 365 / COIN) is restored. It is not
exactly proportional at every boundary (r2 can be 2*r1 +/- 1 due to integer
truncation). That rounding is the on-chain behavior and must not be changed
in consensus code, so relax pos_reward_proportional_to_coinage to allow a
1-unit difference rather than demanding exact doubling. Test-only change.
2026-07-04 15:08:21 -07:00
Krystie 05b56060ab Revert "main: PoS reward proportionality rework — NEEDS CONSENSUS REVIEW"
This reverts commit 2a4da3388f.
2026-07-04 15:03:51 -07:00
Krystie b6b92602ed test: add HD wallet (BIP39/BIP32) coverage
The BIP39+BIP32 key derivation path (hdwallet.cpp) had zero tests despite
being security-critical and required to round-trip keys with the TRIdock
web wallet. Add canonical-vector tests:
- BIP39 Trezor english vector (mnemonic check + seed) and bad-checksum/
  bad-word/bad-length rejection.
- BIP32 spec test-vector 1 (master + m/0H hardened child), verified
  independently by base58-decoding the published xprv.
- DeriveTriangles determinism and index sensitivity.

No implementation changes: hdwallet.cpp derives correctly against the
canonical vectors.
2026-07-04 14:37:21 -07:00
Krystie b3720dbeb6 walletdb: reorder accounting entries across ALL accounts
ReorderTransactions called ListAccountCreditDebit("") which, after the
cursor-scan fix, returns only default-account entries. Entries booked to a
named account therefore kept nOrderPos == -1 forever and sorted incorrectly
in listtransactions. Use the "*" all-accounts sentinel, matching the
listtransactions RPC path and upstream Bitcoin.

Adds regression test acc_reorder_covers_named_accounts (fails on the old
code: named-account entry keeps nOrderPos == -1).
2026-07-04 14:37:21 -07:00
Krystie 2a4da3388f main: PoS reward proportionality rework — NEEDS CONSENSUS REVIEW
DO NOT MERGE without explicit sign-off. This changes GetProofOfStakeReward
rounding (round-half-up vs truncation, and whole-coin truncation of coin
age first). New formula can pay 1 unit more than the old one for some
inputs; un-upgraded nodes would reject such coinstakes — hard-fork risk.
The test-suite proportionality failures it addresses could instead be
fixed by relaxing the test. staking_tests expectations updated to match.
(From prior audit session; isolated here for review.)
2026-07-04 14:18:59 -07:00
Krystie 239cf61795 script: fix signature cache false positives (security)
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.
2026-07-04 14:18:58 -07:00
Krystie c2e05e1305 walletdb: fix SQLite cursor scan dropping accounting entries
ListAccountCreditDebit kept the Berkeley-era early-break on the first
non-acentry record. The BDB cursor was sorted and pre-seeked to the
(acentry, account) prefix via DB_SET_RANGE, so breaking was correct there.
The SQLite cursor (SELECT key, value FROM main) scans the whole keyspace
in unspecified order, so the loop usually hit the version record first
and returned zero entries: every wallet silently lost its accounting
history in the UI. Skip non-matching records instead of breaking.

Fixes all 27 accounting_tests/acc_orderupgrade failures.
2026-07-04 14:18:58 -07:00
Krystie 8d4d17e7a8 test: repair failing unit tests and add consensus safety checks
- Checkpoints_tests: align with the checkpoint map refreshed 2026-07-01
  (2186940 pin superseded by 2205000/2206004 pins).
- wallet_tests: make abandon_not_from_me self-sufficient; add_coin() never
  populated mapWallet, so the test provisions its own not-from-me tx.
- DoS_tests: RFC 6979 deterministic-signing fix (from prior audit session).
- http_seed_tests: correct chunked-body byte math in
  dechunk_split_at_awkward_boundary (\r\r\n is 3 bytes, not 2).
- onion_v3_tests: .onion.onion fix (from prior audit session).
- time_drift_tests: post-fork drift limit is 90s (main.h), not 180s.
- consensus_safety_tests: new suite pinning consensus constants
  (MAX_REORG_DEPTH, MAX_MONEY, fork heights, fee floors, etc.).
- CMakeLists: TEST_DATA_DIR definition quoting fix.
2026-07-04 14:18:58 -07:00
Krystie 5f3982174e qt: align wallet brand colors with logo (#e32105)
The QT wallet source used #f26522 (orange-red) for all UI accents
including tooltips, menus, scrollbars, messagebox borders, HD badge,
and embedded HTML link styling. The actual triangle logo on
cryptographic-triangles.org is #e32105 — confirmed by sampling the
PNG (mode color across 30% of pixels, matching the site's
<meta theme-color>).

This is a global, byte-for-byte replacement:
  #f26522 -> #e32105 (1255 occurrences)
  #61280E -> #3d0e04 (168 occurrences, re-derived hover/active shade)

Touches 99 files: 14 .cpp/.h, 22 .ui forms, 1 plugin .ui, 62 locale .ts.

The 'TRI brand color' comment in updateHDStatus() now references
#e32105 to match the canonical value.

Visual diff against pre-replacement wallet required before merge.
2026-07-04 04:29:02 -07:00
Krystie 2c2efd83fd ci: harden Tor expert bundle downloads against CI egress timeouts
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.
2026-07-03 17:25:16 -07:00
Hermes Agent e2cd0b6057 bootstrap: NeedsBootstrap check for rocksdb/ chain state
The chain DB detection at src/bootstrap.cpp:51-61 checked for txleveldb/,
blocks/chainstate/, and chainstate/ — but not rocksdb/. After the LevelDB
to RocksDB migration completes on v6.1.x, the live chain state lives in
rocksdb/. If the legacy txleveldb/ directory is removed (a reasonable
cleanup operation now that the migration is done), the boot path
incorrectly decides 'no blockchain data found' and triggers a 943 MB
bootstrap download over Tor. DNS2 incident 2026-07-03: 5-hour wedge from
exactly this; recovery via v3 snapshot drop + rm -rf rocksdb + restart.

Add fs::exists(dataDir / "rocksdb") to the OR-chain so a fully-migrated
node stays recognized as 'has chain DB' even after txleveldb/ cleanup.

The four states this handles correctly:
- Fresh node (no chain DB): bootstrap → snapshot → load
- Mid-migration (txleveldb + no rocksdb): don't bootstrap, migrate
- Post-migration (both): don't bootstrap, load RocksDB
- Post-cleanup (rocksdb only, the broken case before this fix): now
  correctly recognized as 'has chain DB' — don't bootstrap, load RocksDB

Ref: references/needsbootstrap-rocksdb-gap-2026-07-03.md (full incident
notes, recovery recipe, defense-in-depth notes on the auto-snapshot
loader at init.cpp:1260 which is already backend-aware).
2026-07-03 13:57:16 -07:00
Krystie 8b7023810b qt(wallet): wire up HD/I2P/Tor status-bar icons
The cherry-pick of updateHDStatus/updateI2PAddress from master left the
TrianglesGUI ctor without the corresponding label_hd / label_i2p /
label_i2p_icon / label_tor_icon wiring, and trianglesgui.h missing the
function declarations. Master compiles because all four exist together.

Add the constructor blocks guarded by findChild so they no-op on
hd-on-master's narrower UI (these widgets aren't added yet) and just-
work when master merges in the I2P-UI work. Add the missing function
declarations to the header.
2026-07-02 23:57:06 -07:00
Krystie c7314b2357 qt(wallet): outline the HD status letters in TRI brand red
Adds OutlinedLabel, a small QLabel subclass that paints each character
with a colored outline and a hollow interior. Used for the HD badge
in the status bar so each letter H and D is bordered in the same

- outline + fill done in custom paintEvent (no QSS hacks)
- updateHDStatus() now drives setOutlineColor/setOutlineWidth
  directly instead of stylesheets
- registered OutlinedLabel as a custom widget in mainwindow.ui
- labelHdIcon pointer type updated to OutlinedLabel*
2026-07-02 23:55:56 -07:00
Krystie 175abcd8a4 test: ResetChainDBStatics() helper to fix chaindb_wipe test isolation
The chaindb_wipe test suite runs after chaindb_backend_selection and
rocksdb_wrapper, both of which leave the process-wide static g_rocksdb
(and on some paths the leveldb txdb singleton) alive. A leaked
g_rocksdb means the next test that does MakeChainDB('cr+') may get a
path that the prior test's open handle is still serving — leading to
the test operating on stale state and the on-disk wipe having no
effect. The H1 crashed_migration_marker_triggers_retry test
specifically could not bootstrap a fresh txleveldb/ for the migration
because the leveldb handle from the prior test was still bound.

This is the same class of bug as W2 (live LevelDB iterator outliving
the DB close) but at the test binary's process-lifetime scale: a live
DB handle from a prior test leaks into the next test and the on-disk
wipe is a no-op.

Fix: add a ResetChainDBStatics() helper that explicitly opens + closes
both backends (in create-if-missing mode so it works whether or not a
prior test left a DB on disk) and then wipes the on-disk chain DB
directories. Call it at the top of every chaindb_wipe test.

Before: 2/4 chaindb_wipe tests passing (H1 retry, H4 happy path) due
to the static-state leak. The crashes were also producing spurious
SIGABRTs at process exit from the static VersionSet assertion.

After: 20/20 chaindb_runtime tests pass, 3/3 chaindb_equivalence,
14/14 snapshotnet.

One file, +52 lines, no production code changes.
2026-07-02 01:38:18 -07:00
Krystie 6cadf7f496 chaindb: W2 iterator-scoping + H4 marker-verify + W1 INADDR_ANY
Three fixes for the chain-DB migration path on real chain data.
All three were uncovered when running the full DNS2 2.2M-block chain
end-to-end; the existing 18 unit tests passed because they exercised
small fixtures, never the real migration entry point.

W2 (root cause): chaindb_migrate.cpp — scope the source.NewIterator()
inside an inner block so it's destroyed BEFORE source.Close(). Live
LevelDB iterators hold a Version ref; closing the DB with one alive
trips the dummy_versions_.next_ == &dummy_versions_ assertion in
leveldb::VersionSet::~VersionSet (version_set.cc:755), aborting the
daemon after verification but before the marker is removed. This
explains the original H4 symptom: the daemon died in the gap between
'verified' and 'fs::remove', and Release builds hid it by compiling
asserts out. The H1 retry path's static-state issue in the test
binary is the same bug at process exit. In-loop failures now break
out with fCopyOK=false and are handled after the iterator dies.

H4 (defense in depth): chaindb_migrate.cpp — keep the verify-and-fail
hardening even though W2 fixes the cause. Use the non-throwing
error_code overload, fs::exists verify after remove, single 100ms
retry (Windows AV/indexer transient locks), hard-fail strError if
the marker still survives. Operator-visible failure beats silent
re-migration time bomb. The H4 invariant: a successful migration
never leaves the marker on disk.

W1: init.cpp — Lookup('0.0.0.0', addrBind, GetListenPort(), false)
replaced with direct CService construction from in_addr{htonl(INADDR_ANY)}.
This was the bug that prevented fc7ad5b from ever starting on
SAMI-PC; Windows getaddrinfo doesn't always map the literal '0.0.0.0'
string to INADDR_ANY.

Test: chaindb_runtime_tests.cpp — adds marker_removed_after_successful_migration
which exercises the real MaybeMigrateLevelDbToRocksDb() end-to-end on
the happy path. Complements the existing
crashed_migration_marker_triggers_retry (retry path). This is the
gap that hid the original bug: no test went through the production
entry point on the happy path.

Runtime verification: full DNS2 chain state (txleveldb 1.1GB +
blk0001.dat 942MB, 6.77M records) migrated end-to-end. MIGRATION_INCOMPLETE
absent from disk after. Reopened rocksdb reads back cleanly via
getblockcount / LoadBlockIndex.

Three files, 152 insertions, 27 deletions, build clean, CI ready.
2026-07-02 01:29:03 -07:00
Krystie f9d1723f6e qt: HD wallet status indicator in status bar
Add a [HD] label next to the lock icon that shows whether the
wallet has a BIP39 HD seed active:
  - Red (#f26522, TRI brand color) when HD is enabled
  - Grey (#555555) when wallet is non-HD (legacy key pool)

Tooltip on hover explains what HD means and what the user must
back up to be able to restore the wallet.

Wired through a new updateHDStatus() slot that reads the
public WalletModel::hdEnabled() accessor and is called when
the wallet model is set. Lives in the icon cluster of the
status bar; the .onion and .b32.i2p address text sits in a
separate group on the right, so no crowding.

Closes the visible-state gap: the wallet already supported
HD seeds (BIP39/BIP32) and had a hdseeddialog, but there was
no visual confirmation of HD status anywhere in the UI.
2026-07-01 21:32:58 -07:00