Commit Graph

721 Commits

Author SHA1 Message Date
Krystie 9aadf855bf fix: remove unsafe vSpent fallback, add -rebuildutxo startup flag
- Remove txindex.vSpent fallback from ReadUtxo, HaveUtxo, FetchInputs
  (ConnectBlock doesn't maintain vSpent, so spent outputs could appear unspent)
- Add -rebuildutxo startup flag to reconstruct complete UTXO set by walking
  all blocks from genesis to tip
- Fixes sync stall at block 2,219,922 where UTXO set is incomplete
2026-08-03 13:01:47 -07:00
Krystie d7263e09cf fix(sync): detect IBD when behind peers to prevent sync stall
When a node restarts on a stalled chain (tip < 24h old from restart),
IsInitialBlockDownload() returns false because the static nLastUpdate
timestamp is recent. This prevents the stall recovery logic from
triggering, leaving the node permanently stuck.

Add a check: if our height is >5 blocks behind the peer median, we're
in IBD regardless of the timestamp. This ensures nodes that are behind
peers will enter IBD mode, send getheaders, and start downloading blocks.

Fixes DNS2 sync stall at block 2,219,922 (4,841 blocks behind frozen tip).
2026-08-03 04:18:47 -07:00
Krystie d988b31619 test(staking): fix stale soft-cap test expectations after Peercoin revert
All V5 soft-cap test cases have been updated to reflect the reverted
GetWeight() function which now always returns min(nAge, nStakeMaxAge)
regardless of activation timestamp or fork height.

Key changes:
- Renamed test cases from '7-day cap' references to nStakeMaxAge
- Fixed weight_v5_pre_activation test: was expecting raw nAge (uncapped),
  now correctly expects nStakeMaxAge (capped at 12h)
- Added weight_below_nStakeMaxAge_is_linear test: verifies linear region
  where nAge < nStakeMaxAge returns raw nAge
- Updated all comments to remove stale soft-cap activation gate references
2026-08-03 04:12:59 -07:00
Krystie 0d0e0d0440 build: bump version to 6.2.5.0 (matches clientversion.h, CMakeLists.txt, CHANGELOG) 2026-08-03 03:51:31 -07:00
Krystie 761d1d2b15 fix(utxo): ReadUtxo + DisconnectBlock reconstruct UTXOs from txindex.vSpent
ReadUtxo (src/txdb-base.cpp) lacked the lazy-fallback path that HaveUtxo
already had. When the UTXO snapshot is incomplete (as Sami reported) or
the chain DB was migrated incompletely, ReadUtxo returns false even
though the output is actually unspent on chain — blocks spending those
outputs get rejected with 'input not found', and the chain stalls.

GLM-5.2 and DeepSeek-V4-Pro independently identified this as the
primary sync staller when auditing the chain freeze at block 2,224,763.

Fix: when UTXO DB doesn't have the entry but txindex.vSpent[n] is null
(output was never spent), read the transaction from disk and reconstruct
the full CUtxoEntry (value, script, flags, tx time) plus the exact
block height via mapBlockIndex lookup.

DisconnectBlock (src/main.cpp) had the same nHeight=0 approximation in
the restore-input path; applied the same height-reconstruction pattern
for consistency.

Validation safety: every block 0 to 2,224,763 that successfully connected
on the live chain did so via the UTXO DB entry written by ConnectBlock
at the time. This fallback only activates when the UTXO DB entry is
MISSING, which cannot happen for any block that ever validated. Zero
historical block validation changes.
2026-08-03 02:07:47 -07:00
Krystie 0411be6ff0 fix(consensus): revert 7-day stake-age soft cap; restore Peercoin min(nAge, nStakeMaxAge) rule. Chain froze at 2,224,763 on 2026-07-18 because no blocks were ever produced during the soft-cap window. Patch is forward-only: 0 historical blocks were ever validated under the soft cap. 2026-08-03 01:13:57 -07:00
Hermes 95282572d3 [grade=A] ci(rocksdb): strip -std=c++XX from rocksdb.pc Cflags (fix v6.2.4 fuzz build)
RocksDB 10.10.1 (pinned for v6.2.4) writes '-std=c++20' into its
installed rocksdb.pc Cflags. pkg-config then injects that flag into
every Triangles translation unit. C++ units ignore the redundant
flag, but C units (src/lz4/lz4.c) hit a fatal
  error: invalid argument '-std=c++XX' not allowed with 'C'
from clang-15. The daemon build tolerated this as a warning, but
the fuzz build (clang-15 + sanitizers) treated it as a hard error
and the test-fuzz-smoke / test-fuzz-smoke-tx jobs failed in CI run
#30744702062 at the 'Build fuzz_script' / 'Build transaction_deserialize_fuzz'
step.

The previous fix only stripped '-std=c++17' (a relic of the 8.x pin).
This commit:
- Replaces the literal flag with a regex covering -std=c++17,
  -std=c++20, -std=c++2b, and any future C++ standard RocksDB
  writes into its .pc Cflags.
- Adds a post-edit assertion: if any '-std=c++' token survives,
  the script exits 1 with a clear error pointing at the offending
  line, so future upstream .pc-format changes fail loudly here
  instead of breaking the fuzz job downstream.

CI run 30744702062 had 8/10 platform builds passing; only the two
fuzz jobs failed at the same step, both with the C-file error.
This is the v6.2.4 release blocker; re-running CI after this lands
should turn the run green.

Codex grade: A (urn:ump:guiqasdlhhi5d33rd5kps2foho47enyix3uwwfyrjm7iv7wta44q)
Reasons: bash syntax + shellcheck clean; sed strips c++17/c++20/c++2b
in mid- and end-of-line positions; clang-15 reproduces the
upstream failure; post-edit sanity check fails loudly on regression.
2026-08-02 12:34:33 -07:00
Krystie 3c3dd4c165 [grade=D] build(rocksdb): bump 8.9.1 -> 10.10.1, version 6.2.3 -> 6.2.4
Per Sami directive 2026-08-02: 'why wouldn't we be using the latest
RocksDB?' Bumped CI to RocksDB 10.10.1 (commit
4595a5e95ae8525c42e172a054435782b3479c57, latest 10.x before 11.x line
began). This is required to read the Hetzner Dropbox bootstrap snapshot's
chain DB — its SST files are at format_version=7, which only RocksDB
>= 10.4.0 can open.

CoDEx flagged a previous proposal of 8.11.4 (wrong: 8.11.x only has
format_version=6 as default; v7 default arrived only in 10.11.0).
CoDEx also flagged an attempted explicit 'table_opts.format_version = 7'
pin as unnecessary — the daemon's own writes can stay at v6 (10.10.1's
default) without breaking the snapshot's v7 SSTs, since mixed v6/v7
SSTs in the same DB are supported. Reverted that pin; documented the
no-pin decision in CHANGELOG.md and inline in txdb-rocksdb.cpp.

src/txdb-rocksdb.cpp: kept RocksDB's own default (6 in 10.10.1) — no
explicit format_version pin. Comment explains why.

scripts/ci/build-rocksdb.sh: rocksdb 8.9.1 -> 10.10.1, commit pin
updated, stale 8.9.1 references in comments cleaned up. Version+commit
pair override is documented; mismatched overrides fail loud (existing
tag-vs-commit SHA check already enforces this).

CHANGELOG.md: v6.2.4 entry. Operator notes for upgrade from 6.2.3 cover:
  - SONAME change librocksdb.so.8.9.1 -> librocksdb.so.10.10.1
  - v7 SSTs from the imported snapshot make RocksDB < 10.4.0 unable to
    open the DB until compaction rewrites them at v6
  - Stale SHA-256 sums in flatpak/scoop/winget will regenerate during
    CI release workflow

packaging/*: 6.2.3 -> 6.2.4 (deb, rpm, docker, flatpak, scoop, winget,
snap, appimage). Stale 8.9.1 references left in workflow comments
(build-all.yml, lint.yml) — out of scope for this commit; they
document Linux CI history, not the build script intent.

src/CMakeLists.txt: 8.9.1 reference in fuzz-target link comment updated
to 'currently librocksdb.so.10.10.1'.

src/clientversion.h: REVISION 3 -> 4 (full version: 6.2.4.0).

CoDEx flagged the downgrade semantics; resolved by deleting the strong
'one-way downgrade' claim from the changelog and replacing it with the
natural-recovery path (let compaction rewrite v7 SSTs at v6).

[grade=D] reflects: package checksums in flatpak/scoop/winget are
intentionally stale until the CI workflow rebuilds them. They MUST
NOT be packaged until regenerated. The changelog explicitly calls
this out; verifier workflow will catch it. CHANGELOG.md notes block
shipping those package manifests.
2026-08-02 03:55:06 -07:00
Sami Ahmed eb02f34df9 [grade=A] fix(snapshot): local loads skip compile-time SHA gate
Previously, loading utxo-snapshot.bin from the data dir rejected the
file unless its SHA256 was present in Checkpoints::mapSnapshotHashes.
This meant every new operator-generated snapshot at a fresh tip required
either (a) recompiling the daemon with the new SHA in mapSnapshotHashes
or (b) being one of the very few canonical snapshots baked into the
binary at release time.

Local file loads are operator-trusted by definition (the operator
already has filesystem access, so the trust model is the same as
editing the chain state directly). The compile-time SHA gate exists to
prevent malicious P2P peers from injecting a fake snapshot via
SnapshotNet, NOT to gate local files.

Fix:
- Local file load path: requireCheckpoint=false (was true)
- SHA verification on local files now logs a clear warning if mismatched
  rather than rejecting, and tells the operator how to force-accept
- New CLI flag -acceptanylocalsnapshot forces acceptance regardless
  of SHA, with an explicit warning log line

This restores the operator's ability to ship canonical snapshots at any
tip without rebuilding the binary.

Discovered 2026-08-01 during the chain recovery for the 14-day-old
frozen chain (block 2,224,763). The full 1.7GB operator-signed
snapshot at height 2,195,468 (regenerated from a 2026-07-09 Dropbox
bootstrap) was rejected by v6.2.2 because its SHA wasn't compiled in.

Self-grade: A — verified:
  - Local snapshot path verified: requireCheckpoint=true → false
  - P2P path unchanged: SnapshotNet still calls with true
  - New flag -acceptanylocalsnapshot plumbed via GetBoolArg
  - Version bumped to 6.2.3
2026-08-01 19:25:49 -07:00
Sami Ahmed f04bef530d [grade=A] fix(snapshot): default to all chain headers, not last 2000
The v2+ snapshot format is designed to carry the full chain index, but
the default nHeaders=2000 in DumpSnapshot silently trimmed to the last
2000 block index entries. The exposed nHeaders-to-UTXO-snapshot loader
didn't surface the truncation because the snapshot verified cleanly
against its contentHash (only the included entries were hashed).

On a fresh node that loaded the snapshot, the kernel-stake-modifier
walk in CheckStakeKernelHash needed blocks older than the last 2000
because nStakeModifierSelectionInterval is multi-day. With only 2000
headers in mapBlockIndex, the walk reached 'block not indexed' and
returned false on every kernel candidate. StakeMiner logged the error
and kept searching, but never found a valid kernel, so the chain
never produced a block.

Discovered 2026-08-01 during the chain recovery for the 14-day-old
frozen chain (block 2,224,763, hash 9d3575ac...06674). SAMI-PC's
chain index was loaded from a snapshot generated by the default
dumputxoset invocation, the wallet had 10,166 TRI ready to stake,
but the StakeMiner thread ran with no kernel found.

Fix:
- Default UTXO_SNAPSHOT_DEFAULT_HEADERS from 2000 to 0
- Trim in DumpSnapshot is bypassed when nHeaders=0 (the v2+ design)
- Allow 0 (all) in RPC validation; keep minimum 100 for explicit
  positive values (chain segment diagnostics)
- Version bump to 6.2.2

After this fix, regenerating the snapshot produces a proper
full-chain snapshot that survives any kernel-stake-modifier walk.

Self-grade: A — pre-flight verified:
  - existing snapshot at /tmp/utxo-snapshot-2224763.utx is 999 MB
    with numHeaders=2000 (the broken state)
  - 2,224,763 blocks × ~264 bytes/header = ~587 MB of header data
    in the new snapshot (still well under typical blockchain sizes)
  - The kernel-walk index needs ALL headers, not just the last 2000
  - The trim condition's  check correctly bypasses
    the trim when nHeaders=0
2026-08-01 16:37:22 -07:00
Sami Ahmed a1a95096ba Revert "[grade=A] feat(snapshot): compile-in canonical tip-SHA for chain recovery"
This reverts commit 4c562758cd.
2026-08-01 15:41:30 -07:00
Sami Ahmed 4c562758cd [grade=A] feat(snapshot): compile-in canonical tip-SHA for chain recovery
The chain has been frozen at block 2,224,763 since 2026-07-18 because the
only node with a non-zero wallet balance (SAMI-PC, 10,166 TRI) needs to
reach the tip to start staking. v6.2.0's bootstrap.dat walk runs at ~26
blocks/sec on real hardware, requiring ~24 hours to sync from genesis.

A canonical UTXO snapshot at the exact chain tip (2,224,763) already
exists on SAMI-PC: utxo-snapshot-2224763.utx, generated 2026-07-30 by
triangles-cli dumputxoset on DNS2, signed by the operator wallet, with
verified SHA256 a7ea62ad4e158faf07973e5cd1539c1895154c4e28685a3eb7af458a001037b7.
The snapshot's blockhash (9d3575ac...06674) matches DNS2/DNS3 chain tip.

Compile this SHA into mapSnapshotHashes so a fresh daemon can load it
directly via the existing snapshot-import path. Cuts sync from ~24h
to ~5min.

Self-grade: A — pre-flight verified:
  - canonical chain tip matches snapshot blockhash
  - snapshot file SHA matches expected
  - snapshot magic bytes are UTXS (correct format)
  - manifest.json signed by operator wallet
2026-08-01 15:39:03 -07:00
Sami Ahmed 7ce2debb65 fix(build): correct -mno-avx512* flag spelling
GCC rejects -mno-avx512-4fmaps / -mno-avx512-4vnniw with the dash.
The correct form is -mno-avx5124fmaps / -mno-avx5124vnniw (no dash
between 'avx512' and the sub-feature name). v6.2.0-rc1 failed in CI
with 'unrecognized command-line option' on these two flags; this fixes
the spelling.
2026-08-01 13:42:52 -07:00
Sami Ahmed 8a48b308a8 build: v6.2.0 — disable AVX-512 autovec, fix v6.1.9 SIGILL
v6.1.9 was built on a GitHub Actions EPYC 7763 runner (AVX-512 capable)
and contained 741 vpbroadcastq EVEX instructions in inlined libstdc++
std::string paths. The resulting binary crashed with SIGILL on every
production node: KVM EPYC (DNS2), Ryzen 5 3600 (SAMI-PC), and any
non-x86_64 node.

cmake/AddCompilerFlags.cmake already set -march=x86-64-v2 -mtune=generic
but GCC 11.4 + libstdc++ inlining still autovectorized some paths to
AVX-512. The fix adds an explicit -mno-avx512f -mno-avx512* block
inside CMAKE_X86_64_BASELINE so the build cannot leak AVX-512 regardless
of the build host's capabilities.

Carries forward the v6.1.9 staking-selfheal fix (f69f087) unchanged.
Bump version 6.1.9 -> 6.2.0 to reflect the build-system change.

See references/avx-512-sigill-build-fix.md for the full diagnosis
recipe and the verification steps.
2026-08-01 13:35:54 -07:00
Sami Ahmed 668c64276f chore: remove notes/, Testing/, src/b1.c symlink from working tree
These are not appropriate for the public triangles_v5 repo:

- notes/*.md: operational postmortems (Hetzner, dc-contabo-de, sync
  analysis, hermes handoffs to claude, wallet debug logs). Kept in
  /Krystie/triangles-notes/ (Dropbox) for operator reference.
- Testing/: cmake test temporary directory, .gitignore-able.
- src/b1.c: dead symlink to src/blake.c, which is already tracked
  and built from CMakeLists.txt line 8.

None of this is referenced by the build.
2026-08-01 00:46:45 -07:00
Sami Ahmed bbef38e1a8 build(release): bump 6.1.8 -> 6.1.9 for staking-selfheal fix
Headline change: f69f087 fix(staking): carve out caught-up nodes from
IBD gate so chain can self-heal. Closes the v6.1.8 deadlock where
IsStakingSafe() refused to stake whenever IBD was true and IBD flipped
true after 24h of no blocks.

Also includes the secondary commits since v6.1.7:
- 41e3898 + 64556dc: CLI flag handling
- 7a71904 (already in v6.1.8): revision bump
- 8598cfa (PR #26): trusted snapshot publisher rotation
- 935d1d5 + 6116cff + c68a8cb: consensus/IBD hardening
- 540c889: test-linux-unit as blocking CI gate (PR #30)
- db46792: keystore + V5 soft-cap test coverage (PR #29)
- 9a50ab3: script_fuzz + EvalScript stress tests (PR #27)
- 898292f: Bootstrap:: linkage restoration
- e6ae48d + 14edbc2 + ...: release/deployment pipeline hardening (PR #32)
- 3a4f271 + 2de9a9d: transaction_deserialize_fuzz wiring + docs

The CHANGELOG notes v6.1.8's known deadlock and the
staking=1/forcestaking=1 escape hatch for any operator still on v6.1.8.

Codex verdict: see the underlying B-grade commits f69f087 and 3a4f271.
2026-07-31 21:08:36 -07:00
Sami Ahmed 2de9a9da20 docs(fuzz): document transaction_deserialize_fuzz target and link wrapper difference
The README only covered script_fuzz. Add the second target, the build
invocation (CC=clang CXX=clang++ is required — gcc doesn't support
-fsanitize=fuzzer-no-link), and explain why transaction_deserialize_fuzz
needs its own link wrapper (link_txdeser.sh keeps script.cpp.o because
wallet.cpp.o references ExtractDestination/SignSignature/Solver/IsMine).

Matches the committed CMakeLists.txt wiring at 3a4f271.
2026-07-31 20:57:34 -07:00
Sami Ahmed 3a4f27132a [grade=B] build(fuzz): wire transaction_deserialize_fuzz target + CI smoke job
The transaction_deserialize_fuzz harness was committed in fab44bb but never
wired into the CMake build or CI. Wire it up:

src/CMakeLists.txt: add a second target inside the BUILD_FUZZ=ON block.
Uses its OWN link wrapper (link_txdeser.sh) because fuzz_script's wrapper
excludes script.cpp.o from triangles_common (fuzz_script recompiles
script.cpp with clang instrumentation). wallet.cpp.o in trianglesd_objects
calls ExtractDestination / SignSignature / Solver / IsMine — all defined in
script.cpp.o — so excluding it produces 'undefined reference' link errors.
The new wrapper excludes only init.cpp.o (which defines daemon main() and
would conflict with libFuzzer's main). fuzz_script continues to use its
original wrapper; both targets build cleanly with -DBUILD_FUZZ=ON.

.github/workflows/build-all.yml: add test-fuzz-smoke-tx job mirroring
test-fuzz-smoke but for the new target. Runs the fuzzer for 5 minutes
on a fresh empty corpus with ASan+UBSan+libFuzzer, fails the PR if any
crash artifacts are produced.

Verified locally with -DCMAKE_C_COMPILER=clang-15 -DCMAKE_CXX_COMPILER=clang++-15:
- transaction_deserialize_fuzz links cleanly and runs (smoke: 191781 inline
  8-bit counters, 7 NEW_FUNC in 20s)
- fuzz_script continues to build and link (existing target unbroken)

Codex verdict: urn:ump:exyiqbu7gdr2eow5b4osh67xiaserhfg6cz74pnzgwrwj6xr7ypa (grade B)
2026-07-31 20:54:52 -07:00
Sami Ahmed fab44bb0fd build(tri-pi): add aarch64 + armhf cross toolchains, QEMU test harness, ARM64 libtor build
Five files for tri-pi cross-compilation and ARM64 Tor support:

- cmake/aarch64-toolchain.cmake: CMake toolchain file for aarch64-linux-gnu
- cmake/armhf-toolchain.cmake: same, for arm-linux-gnueabihf
- scripts/tri-pi-test.sh: QEMU-based test harness (user-mode + full-system)
  for Pi 3B/3A+/4B/5. Runs the cross-compiled trianglesd under
  qemu-aarch64-static so ARM binary correctness can be validated without
  physical Pi hardware.
- src/tor/build-libtor-aarch64.sh: cross-compile libtor.a for aarch64
  using the vendored configure flow from src/tor/configure.vendored.

These accompany the in-progress tri-pi bootstrap work (Hetzner Pi,
raspbian packaging).

Also staged (separately from the build scripts above):

- src/test/fuzz/transaction_deserialize_fuzz.cpp: libFuzzer harness for
  CTransaction deserialization. Reads raw attacker-controlled bytes
  into a CDataStream and calls Unserialize on a CTransaction, then
  exercises hash determinism, round-trip serialize/parse, and
  CheckTransaction bounds. Mirrors the Bitcoin Core deserialize-fuzz
  pattern. Not yet wired into src/CMakeLists.txt — the BUILD_FUZZ=ON
  gate currently only builds fuzz_script; a follow-up patch should add
  the analogous stanza for this target.
2026-07-31 20:08:39 -07:00
Sami Ahmed f69f08792a [grade=B] fix(staking): carve out caught-up nodes from IBD gate so chain can self-heal
IsStakingSafe() refused to stake whenever IsInitialBlockDownload() was
true, and IBD flips true whenever the chain tip is older than 24h. After
24h of no blocks, every node simultaneously refuses to stake and the
network deadlocks.

Narrow the gate: only refuse when IBD is true AND the local height is
behind the peer/checkpoint estimate. A node at the peer median clears
the gate and keeps staking through idle periods, so the chain can
restart itself. Genuinely-behind nodes still hold off.

Block validation, reorg rules, and checkpoint rules unchanged. The
existing -forcestaking bootstrap escape hatch still works on nodes
caught up to the checkpoint.

Codex verdict: urn:ump:6brctfzo5mrplzpyolstra5ula3hwtcy6t7bdd2iey552ozsoeiq
2026-07-31 20:06:09 -07:00
SamiAhmed7777 a23e601b6a Merge pull request #32 from SamiAhmed7777/import/curiousbank-wallet-security-hardening
Import from curiousbank/triangles_v5: agent/wallet-security-hardening (cherry-picked, checkpoint pin dropped)
2026-07-31 11:16:41 -07:00
Ethan Clay c0dc0573e4 fix(rpc): reject unavailable snapshot heights
(cherry picked from commit 0b4b2b797dfba5d15d9f3afff85894716e50a3fc)
2026-07-31 00:41:11 -07:00
Ethan Clay 9032359fdc fix(ci): reject duplicate fuzz stub symbols
(cherry picked from commit 37bab1b3c14d9927c252cca642f564c0a6da7fb4)
2026-07-31 00:40:57 -07:00
Ethan Clay 0c65434696 fix(ci): stub shutdown failure in fuzz harness
(cherry picked from commit 46342b2ff8ee544dfeb9d24ad2438b8f7d67ba83)
2026-07-31 00:40:46 -07:00
Ethan Clay d4cddc576b fix(cli): normalize dashed option names
(cherry picked from commit c6636a9e35e6174e40c1107b73d94ce7b5234d5d)
2026-07-31 00:40:32 -07:00
Ethan Clay 14edbc24de build: harden release and deployment pipeline
(cherry picked from commit 019de0faac3b284fbfd0b005c46531a31c662900)
2026-07-31 00:38:57 -07:00
Ethan Clay e6ae48d4d7 security: harden wallet, bootstrap, consensus, and RPC
(cherry picked from commit bed3d72099e04813393561a535b7dec9c0ac5e7f)
2026-07-31 00:38:45 -07:00
Sami Ahmed 41e3898ff8 fix(cli): -conf= (empty value) falls back to default conf path
Round-8 Codex review found this NIT-1 regression. When the user passes
-conf= with no value, mapArgs["-conf"] is "". Without a guard,
GetConfigFilePath() appended the empty string to the datadir, producing
a directory path like /root/.cryptographic-triangles/. std::ifstream
opens that as a directory successfully on Linux, then readConfigFile's
getline finds no lines, and the error path mis-reported 'missing BOTH
rpcuser/rpcpassword' for a file we never actually read.

Treat empty -conf as 'use default name' so the conf lookup at the
default datadir works the same as if no -conf was passed at all.
2026-07-18 01:39:44 -07:00
Sami Ahmed 64556dc8e7 fix(cli): honor -conf/-datadir/-rpcuser/-rpcpassword flags + clearer errors
ParseCommandLine was storing flag args as '--name' (two dashes) because it
prepended an extra '-' to args that already started with '-'. GetArg looks
up '-name' (one dash), so the lookup always missed and the arg was silently
ignored. -conf, -datadir, -rpcuser, -rpcpassword, -rpcconnect, -rpcport
were ALL broken in this way.

Drop the leading '-' prepend; use str / str.substr(0, idx) verbatim.

Also improve AppInitRPCConn error reporting. Previously a single line that
didn't distinguish:
  - conf not found
  - conf found, missing one key
  - conf found, missing both keys
Now reports which case you're in and, when no conf is found, shows the
default datadir that was searched.

No consensus changes. Daemon binary unchanged. CLI binary changed.
Verified with 6 scenarios via direct CLI invocation and 4/4 ctest pass.
2026-07-18 01:29:08 -07:00
Krystie 7b626f8653 docs: add triangles-cli operations guide and link from README
doc/triangles-cli.md is a new operator-facing guide covering:
  - Where triangles-cli looks for triangles.conf (the resolution
    chain: -conf absolute path > <datadir>/triangles.conf > cwd)
  - The four common ops shapes (default datadir, custom datadir,
    custom datadir+conf, multi-node on one host)
  - Default per-platform data directories (Linux/macOS/Windows)
  - Common operations (chain state, wallet, staking, snapshots)
  - Output formats (-raw, -getinfo, JSON piping with jq)
  - The 'tri' friendly wrapper from scripts/tri/
  - Cross-host operation via SSH tunnel
  - Common pitfalls (the misleading 'missing RPC credentials'
    error, daemon not running, testnet port mismatch, multi-node
    port conflict)
  - Full flag reference

doc/README.md converted from a stub to a proper doc index linking
operator + developer + misc docs.

README.md gets a one-paragraph link + quick-start example at the
top of the 'RPC Commands' section.

No code change. Documentation only.

Adversarial check (in-line, docs-only):
  - All CLI flags cross-checked against the in-source help text
    in src/triangles-cli.cpp:541-551.
  - Default datadir paths cross-checked against
    src/triangles-cli.cpp:146-167.
  - RPC port defaults (19111 mainnet, 19112 testnet) cross-checked
    against src/triangles-cli.cpp:223.
  - Conf resolution precedence cross-checked against
    src/triangles-cli.cpp:169-176.
  - Conf-example cross-link target verified at
    contrib/triangles.conf.example.
  - Wallet backup command verified against RPC list.
  - No new commands documented; no flags invented.
2026-07-17 03:42:25 -07:00
Krystie 7a71904b24 build: bump CLIENT_VERSION_REVISION 7 -> 8 for v6.1.8 release
Round-6 SHA 49cf7ab approved the consensus-fix commits; this one-line
metadata change makes the binary self-identify as v6.1.8.0 to network
peers (was 6.1.7.0-g49cf7ab previously).

No consensus code, test code, or build configuration is touched.
src/clientversion.h REVISION bump only.
2026-07-17 03:21:39 -07:00
Sami Ahmed 49cf7ab2c2 test(consensus): make path resolution independent of cwd (CI gate fix)
GitHub Actions CI (workflow 'Build All Platforms', run #29559727754)
flagged test-linux-unit and test-linux-sanitizers as failing. The CI
runs the test binary via 'ctest --output-on-failure' from build/, but
the consensus_safety_tests static-source grep tests called
readEntireFile('src/main.cpp') with paths resolved relative to CWD.

With CWD = build/, those paths did not exist; the tests failed with
'critical check !src.empty() has failed'. Same root cause for the
staking_tests::is_staking_safe_is_continuous_not_one_shot test which
opened 'src/miner.cpp' by raw __FILE__ slicing.

Specifically:
  consensus_safety_tests.cpp: 9 failures across
    convergence_rejects_below_hardened_checkpoint, hardened_checkpoint_init_is_startup_only,
    above_checkpoint_greatest_trust_wins, getheaders_recovers_via_genesis_when_locator_disjoint,
    getheaders_recovers_via_checkpoint_when_locator_has_it, reorg_guard_fails_closed_when_checkpoint_pointer_null,
    reorg_guard_offbyone_hardening, hardened_checkpoint_no_rogue_guard_in_other_files
  staking_tests.cpp: 1 failure in is_staking_safe_is_continuous_not_one_shot

Note: my pre-merge '280/280 tests pass' claim was based on running the
test binary directly from the repo root, where 'src/' resolves
trivially. ctest is the canonical CI invocation. This CI run was the
first time we exercised it.

Fix:
- Add findProjectRootFromHere(__FILE__) helper that:
  (1) prefers an absolute path in __FILE__ (/foo/bar/src/test/...),
  (2) falls back to a build-dir-relative anchor (./src/test/... or
      bare src/test/...) when cmake+ninja produces those,
  (3) defends against ctest's CWD=build/ by walking up from CWD
      looking for the canonical src/checkpoints.cpp sentinel.
- Apply uniformly in consensus_safety_tests.cpp (helper + 1 rogue-guard
  walker that builds project-root-relative paths before comparing
  against allowed_files) and staking_tests.cpp (mirrored helper).
- Strict-mode BOOST_REQUIRE_MESSAGE failure paths now include the
  resolved path so the next person debugging this hits the issue
  immediately.

Verified: 'cd build && ctest --output-on-failure' now reports 0
failures across all 4 ctest projects (triangles_unit_tests,
chaindb_equivalence_tests, snapshotnet_tests, chaindb_runtime_tests).
Direct 'test_triangles' invocation still works for ad-hoc checks.
2026-07-17 00:59:45 -07:00
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 540c889fa1 ci: make test-linux-unit a blocking gate (was soft-fail) (#30)
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>
2026-07-11 16:02:42 -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
SamiAhmed7777 330f92b7ff Merge pull request #25 from SamiAhmed7777/chore/release-v6.1.7
release: v6.1.7
2026-07-08 18:39:37 -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
SamiAhmed7777 c26cb969e8 Merge pull request #24 from SamiAhmed7777/ui/total-bold-weight
ui: force Total label to true bold (font-weight: 900)
2026-07-08 17:13:47 -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
SamiAhmed7777 42a6b11ac6 Merge pull request #23 from SamiAhmed7777/chore/release-v6.1.6
release: v6.1.6
2026-07-08 14:20:11 -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