Compare commits

...

82 Commits

Author SHA1 Message Date
sami7777 41ba9f8bc9 checkpoints: continuous finality pins every 1000 blocks
Adds 8 new hardened checkpoints at heights 2206500-2214400, verified
against the canonical chain. Closes the 8,400-block unverified gap
between the last hardcoded checkpoint (2206004) and the live tip
(2,214,476).

Without these, a fresh node syncing from zero with NO snapshot has
zero finality protection above height 2206004. A peer feeding fork
blocks at heights 2206005-2214400 could trick the IBD node into
accepting a divergent chain, because CheckHardened() only fires at
the exact heights in mapCheckpoints.

With continuous pins every 1000 blocks, any divergence >1000 blocks
is rejected at AcceptBlock time with DoS=100, protecting from-zero
sync against low-trust forks.
2026-07-01 03:46:26 -07:00
sami7777 cbb189aade anti-spam: fix inverted condition + soft scoring
Two bugs in the anti-spam heuristic at src/main.cpp:

1. Inverted comparison: condition was bnNewBlock > bnRequired paired with
   "too little proof-of-stake" error message. The condition triggers when
   the block has MORE difficulty than required (harder than allowed),
   but the message claims the OPPOSITE. Honest blocks during legitimate
   time-warps (fork recovery, chain catchup) get mislabelled.

2. Misbehaving(100) was a single-shot instant ban: banscore threshold
   defaults to 100, so the FIRST anti-spam violation triggered a 24-hour
   ban on every honest peer feeding us blocks during fork divergence.
   This is what caused the 2026-06-23 DNS2 clearnet-fork incident:
   peers got banned before we could determine which chain was canonical.

Fix: condition now correctly says bnNewBlock < bnRequired (block too
easy = reject), and Misbehaving score dropped from 100 to 5 (needs
~20 anti-spam violations before the 100 banscore threshold). Anti-spam
is a soft signal, not a hard ban trigger.
2026-07-01 03:32:04 -07:00
sami7777 5635cb5e57 build: enforce -march=x86-64-v2 on Linux x86_64
GCC 11+ on Intel CI runners (Skylake-X, Ice Lake, Sapphire Rapids)
emits AVX-512/AVX10 instructions for std::string / memcpy inlining
that crash with SIGILL on AMD EPYC and older Intel without those
extensions. Root cause: libstdc++ is statically linked into the
binary, so the build host's instruction set becomes a hard runtime
requirement.

The CI binary crashed immediately on DNS2/DNS3 (AMD EPYC Milan) with:
  traps: trianglesd[...] trap invalid opcode ip:...e432 error:0
  in trianglesd[...+af3000]
Disassembly of the crash site (file offset 0x15b432):
  62 f1 7f 08 6f 41 ff   vmovdqu8 -0x10(%rcx), %xmm0
This is an AVX10/AVX-512 instruction emitted inside
std::basic_string::basic_string (statically linked libstdc++).

Fix: -march=x86-64-v2 -mtune=generic for all Linux x86_64 builds.
v2 baseline (SSE4.2 + POPCNT + CMPXCHG16B) is from 2009 Nehalem and
supported on every x86_64 CPU we ship to. Override-able via
-DCMAKE_X86_64_BASELINE=OFF if a CPU-specific build is needed.
2026-07-01 03:26:31 -07:00
sami7777 b6feab8e94 snapshot v3: carry setStakeSeen over in dump/load
Adds v3 snapshot format that includes the last 5000 PoS block
(prevoutStake, nStakeTime) pairs so a snapshot-loaded node has its
stake-collision set restored without walking the block index.

v2 readers still load v3 snapshots (the extra field is past numBlocks
and the loader checks version >= 3 to read numStakeSeen).

Bumps version to 6.1.1.
2026-07-01 02:17:51 -07:00
Krystie 333f7abfc0 seed: add SAMI-PC I2P address as primary hardcoded seed
fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p
2026-06-30 17:53:55 -07:00
Krystie eb20edf890 seed: add SAMI-PC as primary hardcoded onion seed
6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion
is the authoritative wallet node — must be in every release.
2026-06-30 17:44:09 -07:00
Krystie ff7a7d3b6b build: bump version to 6.1.0
SQLite wallet (default), RocksDB chaindb (default), Boost removal
from common link, I2P startup performance fix (180s→27ms).

Migration is automatic and non-destructive:
- Wallet: BDB → SQLite on first open (original preserved as .bdb.bak)
- ChainDB: LevelDB → RocksDB on next start (via -chaindb flag)
2026-06-30 15:54:45 -07:00
Krystie fb5f1be032 ci: add sqlite3 to Windows MSYS2 install (daemon+qt)
cpp20 modernization made SQLite the default wallet backend
(find_package(SQLite3 REQUIRED)). The windows-qt job got it
transitively via Qt5-base, but the daemon job had no such
dependency → CMake configure failure.
2026-06-30 15:32:29 -07:00
Krystie 83a66814b0 fix: remove dead 'using namespace boost;' from init.cpp
The cpp20 rebase removed all Boost includes from init.cpp but left
this orphaned using-directive. It compiled on Linux daemon (I2P
embedded transitively provides the boost namespace) but broke every
Qt/Windows/macOS build in CI with: 'boost' is not a namespace-name.
2026-06-30 15:12:00 -07:00
Krystie ae7beb0df7 fix(i2p): move StartI2P to background thread — fixes splash screen freeze on first run
i2p::api::StartI2P() calls NetDb::Start() which triggers Reseed() — a
blocking HTTPS download from public I2P reseed servers. On first run
(empty netDb), this blocks for up to 180s, freezing the GUI splash
screen on "Starting embedded I2P router..." indefinitely.

Fix: Move StartI2P + client::context.Start + SOCKS/SAM readiness polling
into a detached background thread. InitI2P (fast, <1s) remains
synchronous. The main init thread proceeds immediately — Tor-only mode
works while I2P bootstraps asynchronously (1-5 min for first run).

Verified: i2p_start startup perf dropped from 180s+ to 27ms on first run
with empty netDb. Reseed + tunnel building continues in background.
2026-06-30 13:58:38 -07:00
Triangles Dev d4d0ddf849 init: skip post-migration Berkeley verify on SQLite wallet
Live-validation of the CWalletDB rebase (preserved snapshot wallet from
DNS2's 2026-04-23 recovery, copied to /tmp/cpp20-validate, run with the
new build) revealed that after MaybeMigrateBerkeleyWalletToSQLite()
converts wallet.dat from Berkeley DB to SQLite, the next block of init
code still calls bitdb.Verify() on the now-SQLite file. BDB cannot open
a SQLite file, so Verify returns RECOVER_FAIL, which init treats as a
fatal InitError ("wallet.dat corrupt, salvage failed"). The migration
itself succeeds and produces a valid SQLite wallet.dat + a
wallet.dat.bdb.bak copy of the original, but the daemon refuses to
continue past Step 5.

Fix: snapshot IsSQLiteFile(walletPath) AFTER the migration hook, then
gate the entire BDB-only Step 5 path (bitdb.Open, salvagewallet,
zapwallettxes, bitdb.Verify) on the snapshot. Once the wallet is SQLite
— whether by migration or because it was born that way — the BDB
verify/salvage steps are skipped; SQLiteDatabase::Open() already runs
PRAGMA integrity_check during connection setup, so the wallet is
validated against the SQLite schema before any handle is constructed.

The verify_db performance log now includes wallet_is_sqlite=N for
observability.

Validation: full migration cycle completed in the test datadir, RPC
getwalletinfo/getaddressesbyaccount returned valid data matching the
pre-migration BDB wallet exactly (5 unique HD-derived addresses,
keypoolsize=101, keypoololdest=1781772890, walletversion=60000).
2026-06-30 01:25:10 -07:00
Triangles Dev 3566eed9e1 wallet: rebase CWalletDB onto CWalletBatchTyped (SQLite default)
Move CWalletDB off the Berkeley CDB base class and onto the typed batch
seam introduced by walletdb-batch.h / walletdb-{factory,sqlite}.{h,cpp}.

Build seam
----------
* CWalletDB now derives from CWalletBatchTyped. The typed Read/Write/
  Erase/Exists templates come from the seam; their bodies (WriteTx,
  WriteKey, WriteMasterKey, ReadPool, WriteSetting, ...) are unchanged
  because only the base class swapped — the call signatures resolve to
  the same templates.
* CWalletBatchTyped takes ownership of the WalletDatabase so the
  underlying connection outlives any batch issued by it (SQLiteBatch
  holds a reference, not a value). The two-phase Open() pattern lets
  CWalletDB hand the freshly opened database to the base class after
  MakeWalletDatabase() returns.
* MakeWalletDatabase (walletdb-factory.cpp) routes -walletdb=sqlite to
  SQLiteDatabase, returning nullptr with a clear error for the
  unfinished Berkeley branch. The CWalletDB constructor surfaces that
  error string on failure.

Cursor sites (the only Berkeley-specific call sites)
---------------------------------------------------
Three sites used GetCursor()/ReadAtCursor() directly:
  * LoadWallet       — full scan, now uses StartCursor()/NextRecord()
  * ListAccountCreditDebit — used DB_SET_RANGE + DB_NEXT loop; replaced
    with full keyspace scan + filter-in-loop (SQLite cursor does not
    support keyed range seeks). Behaviour matches Berkeley: terminates
    when strType changes or, in single-account mode, when
    acentry.strAccount differs.
  * ZapWalletTx — moved to BerkeleyZapWalletTx (see below) because it
    operates on raw Berkeley Db/Dbc/Dbt now that CWalletDB is on the
    seam.

The 3 unused public methods on the old CWalletDB (GetAtCursor /
GetTxnCursor / GetAtActiveTxn) had no callers outside walletdb.{h,cpp}
(verified by grep) and were removed.

Berkeley-only escape hatches
----------------------------
Recover(CDBEnv&,...) and ZapWalletTx(...) became BerkeleyRecoverWallet
and BerkeleyZapWalletTx in a new walletdb-recover.{h,cpp} pair. They
operate directly on DbEnv/Db/Dbc/Dbt because CDB's members are
protected (free functions cannot use the wrapper). The recovery logic
duplicates a BDB-only ReadKeyValue variant locally to avoid pulling
the typed batch seam into a Berkeley-only file.

Init.cpp uses these via:
  * -salvagewallet   -> BerkeleyRecoverWallet(bitdb, ..., fOnlyKeys=true)
  * -zapwallettxes   -> BerkeleyZapWalletTx(...)
  * bitdb.Verify     -> BerkeleyRecoverWallet as the recover callback

Wallet migration hook
---------------------
After the Berkeley verify/salvage/zap steps and before CWalletDB is
opened for the live wallet, init.cpp now calls:

    if (ResolveWalletDbKind() == SQLite &&
        !IsSQLiteFile(walletPath))
        MaybeMigrateBerkeleyWalletToSQLite(walletPath, err)

The migration code (walletmigrate.{h,cpp}) is unchanged — it opens a
private Berkeley environment over the wallet directory, copies every
record verbatim (raw key/value bytes) into a fresh SQLite file,
verifies the row count, then atomically renames the BDB original to
"<name>.bdb.bak" and the SQLite file into place. On any failure the
BDB original is left exactly as it was. Errors surface through
InitError so the daemon refuses to start with a corrupt wallet rather
than silently falling back to Berkeley.

No working Berkeley fallback
----------------------------
MakeWalletDatabase returns nullptr for the Berkeley branch, so
-walletdb=bdb no longer opens a working wallet through the seam. This
is intentional for this release — the migration hook handles existing
BDB wallets at first startup, after which the on-disk file is SQLite
and the BDB code path becomes pure recovery glue.

Header fallout
--------------
walletdb.h no longer pulls in db.h (which would drag <db_cxx.h> into
every TU that includes the wallet API). Forward decls added for
CWalletTx, CBlockLocator, CWallet, CPubKey, CScript, CMasterKey,
uint160, uint256. nWalletDBUpdated is now extern-declared in
walletdb.h and defined in db.cpp (was previously declared in db.h).

Validation
----------
Build: GREEN with USE_TOR_EMBEDDED=ON USE_I2P_EMBEDDED=ON. 6 binaries:
trianglesd, triangles-cli, test_triangles, test_chaindb_runtime,
test_chaindb_equivalence, test_snapshotnet.

Tests: 107/107 + 10/10 + 5/5 = byte-identical to the 5d9da84
baseline. wallet_tests and accounting_tests inside test_triangles now
exercise the SQLite path for the first time — their pass is the
de-facto wallet-migration validation at the test-suite level.
2026-06-30 01:01:16 -07:00
Hermes 3473e80876 Replace boost::program_options config parsing with std::ifstream+getline
Modernizes triangles.conf parsing to use plain stdlib instead of
boost::program_options::detail::config_file_iterator.

Supports the syntax that actual triangles.conf files use:
  - key=value or key = value (whitespace around = is ignored)
  - # comments and blank lines are skipped
  - Surrounding double quotes are stripped from values
  - Same InterpretNegativeSetting semantics
  - Command-line settings still take precedence (don't overwrite existing)

Intentionally NOT supported (different from Boost):
  - Backslash line continuations
  - Escape sequences inside quoted values
  - Section headers ([section])

After this change, triangles_common no longer uses Boost.ProgramOptions
directly. The Boost.ProgramOptions link inside the USE_I2P_EMBEDDED block
remains because i2pd-src/libi2pd/Config.cpp uses it internally.

Verified:
  - Build with -DUSE_I2P_EMBEDDED=ON succeeds
  - Build with -DUSE_I2P_EMBEDDED=OFF succeeds
  - Without i2p, ldd on trianglesd shows NO libboost_program_options dep
2026-06-29 21:47:51 -07:00
Hermes a48fb88e4c Merge remote-tracking branch 'origin/master' into cpp20-modernization-from-pc
# Conflicts:
#	.github/workflows/build-all.yml
#	.github/workflows/lint.yml
#	src/CMakeLists.txt
#	src/bootstrap.cpp
#	src/init.cpp
#	src/test/chaindb_runtime_tests.cpp
#	src/trianglesrpc.cpp
#	src/txdb-factory.cpp
#	src/txdb-leveldb.cpp
#	src/txdb-rocksdb.cpp
#	src/txdb-rocksdb.h
#	src/util.cpp
#	src/walletdb.cpp
2026-06-29 21:10:31 -07:00
Hermes bfdb399772 WIP: modernization applied to DNS2 tree
Brings in uncommitted work from SAMI-PC E:\repos\triangles
cpp20-modernization branch:
- RocksDB default chain DB + auto-migrate from txleveldb
- SQLite default wallet + non-destructive migration from Berkeley
- Boost.Asio removed from RPC (rpc_httpsocket.h)
- Boost removed from all daemon + GUI code
- New: walletdb-base.h, walletdb-batch.h, walletdb-sqlite.{h,cpp},
  walletdb-factory.{h,cpp}, walletmigrate.{h,cpp}
- New tests: chaindb_runtime_tests, chaindb_equivalence_tests,
  snapshotnet_tests
- Docs: BOOST-REMOVAL.md, ROCKSDB-DEFAULT-MIGRATION.md,
  WALLET-SQLITE-MIGRATION.md

Does not yet build — needs CWalletDB->CWalletBatchTyped rebase in
walletdb.cpp/wallet.cpp/db.cpp and merge with origin/master for
v6 source files (checkpointpublisher, tor/, snapshot/, utxosnapshot,
bootstrap.cpp).

Build flags: -DBUILD_QT=OFF -DUSE_I2P_EMBEDDED=OFF
2026-06-29 20:08:06 -07:00
Krystie c577fb2ff5 fix: remove leftover process-I2P calls in shutdown/startup blocks
Merge left StopI2P() (duplicated StopEmbeddedI2P), StartI2P(),
CI2PProcess::GetInstance(), and I2P_DEFAULT_SAM_PORT references from
the SAMI-PC process-based I2P. Replaced with the v6 embedded I2P API:
- shutdown: single StopEmbeddedI2P() (was called twice plus StopI2P)
- startup: single StartEmbeddedI2P() which reads its own args
- removed manual SAM host/port resolution (StartEmbeddedI2P handles it)
2026-06-29 19:18:41 -07:00
Krystie b6b3f3877f fix: remove duplicate labelI2PAddress declaration in trianglesgui.h
Merge left two declarations of labelI2PAddress (lines 113 + 115),
causing cascading type errors on macOS/clang.
2026-06-29 15:31:05 -07:00
Krystie 9ed79d53a6 fix: replace CI2PSession (process-I2P) with CI2PEmbedded in merged code
Merge left residual references to the SAMI-PC process-based I2P API
(CI2PSession, fI2P) in files that now compile against the v6 embedded
I2P (CI2PEmbedded). Fixed:
- net.cpp ConnectNode: removed fI2P/CI2PSession blocks, restored
  v6 SOCKS-proxy connection path (I2P routing handled in netbase)
- CMakeLists.txt: removed i2p.cpp/i2p_process.cpp from build (not
  part of embedded I2P; kept in tree as reference only)
- rpcnet.cpp: CI2PSession → CI2PEmbedded (IsRunning/GetI2PAddress)
- rpcwallet.cpp: same API migration
- init.cpp: same API migration for startup address print
2026-06-29 15:21:11 -07:00
Krystie 8615e6b46d Merge SAMI-PC hd-wallet + process-I2P into v6 master
Merges the HD wallet work and process-based I2P integration from the
SAMI-PC hd-wallet branch into v6 master. Conflict resolution keeps
v6 embedded I2P (CI2PEmbedded) as primary, includes process-I2P
files for reference, preserves FastImportBlockFile() from hd-wallet,
and keeps v6 version numbers (6.0.0) and wAddressStack Qt layout.
2026-06-29 14:52:53 -07:00
sami7777 e694a189f8 Merge hd-wallet into master: I2P process integration + HD wallet + reconcile with origin/master v5.9.15 2026-06-29 13:50:10 -07:00
sami7777 2aeae07d0b feat: I2P integration (process-based) + updated icons + Qt UI for I2P address display 2026-06-29 13:45:59 -07:00
Krystie fcfa3b9938 fix(i2p): flush stdout + set running flag early so UI shows status 2026-06-28 23:00:48 -07:00
Krystie 01f3fdf2ff ci: produce portable Windows GUI wallet ZIP (was missing from release) 2026-06-28 20:17:47 -07:00
Krystie baa9e0a650 fix(i2p): populate .b32.i2p address — was never set, status bar always empty
i2pHostname was cleared on Start() but never populated, so
GetI2PAddress() always returned empty and the Qt status bar never
showed the I2P address even when the router was running.

Now queries i2p::context.GetRouterInfo().GetIdentHash().ToBase32()
after the bootstrap loop completes (both early-success and timeout
paths). The address appears as <hash>.b32.i2p in the status bar.
2026-06-28 18:05:01 -07:00
Krystie ba9cb89a97 fix(i2p): Windows find_library instead of hardcoded .a paths
libboost_system-mt.a doesn't exist on MSYS2 (header-only in newer
Boost). find_library auto-discovers the actual filenames and skips
any that don't exist. Resolves both the missing-file error and the
naming ambiguity.
2026-06-28 17:22:41 -07:00
Krystie ede72d8e8a fix(i2p): Windows link by full static .a paths like i2pd's own Makefile
MSYS2 MinGW doesn't create CMake imported targets for Boost, so
Boost::filesystem etc. silently don't link. i2pd's own Makefile.mingw
solves this by referencing full paths like /mingw64/lib/libboost_*.a.
We do the same — auto-detect MINGW_PREFIX (/mingw64) and link the
exact .a files for boost_filesystem, boost_program_options,
boost_system, openssl, and zlib.
2026-06-28 17:05:50 -07:00
Krystie 8e6c6b36bf fix(i2p): Windows link order — i2pd archives + Boost/zlib sandwich
The linker needs to see i2pd archives, then Boost/zlib to resolve
their symbols, then i2pd archives AGAIN to resolve any remaining
references. Added raw -l fallbacks for MinGW where Boost:: CMake
imported targets may not exist even though libs are installed.
2026-06-28 16:47:21 -07:00
Krystie 045bc36716 fix(i2p): link ordering + optional Boost filesystem/system
Windows: Replace --start-group/--end-group (CMake mis-orders them
with Ninja generator) with double-listing of i2pd static archives.
Linker resolves circular deps in two left-to-right passes.

macOS: Boost 1.90 via Homebrew doesn't provide filesystem/system as
separate COMPONENTS. Use OPTIONAL_COMPONENTS so find_package doesn't
fail, then guard the target_link_libraries with if(TARGET Boost::...).
2026-06-28 16:27:53 -07:00
Krystie a55e45ac1a fix(cmake): add filesystem+system to find_package(Boost) for I2P
The Boost::filesystem and Boost::system targets don't exist unless
find_package(Boost COMPONENTS ...) explicitly lists them. I2P's link
section references them but they were never found, breaking all
platforms.
2026-06-28 16:08:38 -07:00
Krystie bfb422417d fix(i2p): link boost_filesystem + boost_system (i2pd uses boost::filesystem)
libi2pd.a references boost::filesystem::detail::status, exists,
create_directories, etc. These are in boost_filesystem, which the
I2P link section was missing. Added Boost::filesystem and
Boost::system.
2026-06-28 15:55:54 -07:00
Krystie 7e359c0f21 fix(i2p): Windows interface macro conflict + macOS OpenSSL path
Windows: MinGW's rpcndr.h #defines 'interface' as 'struct' (COM
support). i2pd's I2CP.h uses it as a parameter name, causing
parse errors. Add #undef interface before i2pd includes.

macOS: i2pd Makefile.homebrew hardcodes openssl@3.5 but Homebrew
installs openssl@3. build-libi2pd.sh now detects the actual path
and passes SSLROOT=<path> to make, which overrides the Makefile
assignment.
2026-06-28 15:37:57 -07:00
Krystie ba3d7a766a ci: enable embedded I2P (i2pd) on all build platforms
Adds libi2pd static library build step and -DUSE_I2P_EMBEDDED=ON to
all 5 build jobs (Windows Qt, Windows daemon, Linux Qt, Linux daemon,
macOS). Previously I2P compiled as stubs — wallet shipped without
.b32.i2p address support. macOS uses HOMEBREW=1 for correct i2pd
Makefile include paths.
2026-06-28 15:19:52 -07:00
Krystie e16d3b2fb2 fix: column-family RocksDB::Open uses SFINAE wrapper (DB** vs unique_ptr<DB>*)
The CF Open overload had the same DB** vs unique_ptr<DB>* API drift
as the non-CF version, but was calling rocksdb::DB::Open directly
instead of through the SFINAE wrapper. On MSYS2 MinGW (Windows CI)
the unique_ptr-only overload causes a compile error. Added
OpenRocksDBCF with the same int/long SFINAE pattern.
2026-06-28 14:02:28 -07:00
Krystie ba9a825ea4 Merge branch 'master' of https://github.com/SamiAhmed7777/triangles_v5
# Conflicts:
#	src/tor/build-libtor.sh
2026-06-28 13:04:06 -07:00
Krystie 1ec7306e1d qt: stack I2P address above Tor address in status bar; click-to-copy each
Replaces the single label_onion item inside the wStatusBar layout with a
vertical group (wAddressStack) containing two rows:
  row 1: [I2P] <.b32.i2p address>
  row 2: [Tor] <.onion address>

Both address labels now copy their text to the clipboard on click via
the existing eventFilter pattern (extended to handle labelI2PAddress in
addition to labelOnionAddress). The label_i2p, label_i2p_icon, and new
label_tor_icon widgets live in mainwindow.ui so they share the same
layout stretch and ordering as the rest of the status bar; the
QWidget/QVBoxLayout/QHBoxLayout nesting keeps the stack compact and
centered on the existing 37px -> 52px status bar height bump.

Tooltips updated to "Click to copy" for both addresses (no longer
"Selectable - right-click to copy") to match the actual behaviour.
Tooltip wording for [Tor] chip matches the existing [V3] chip.
2026-06-27 21:54:13 -07:00
Krystie 63e33a1569 v6.0.0: bump version after I2P+compact-blocks+rocksdb-cf+fork-det+snapshot-sig
Major release. All v6 features landed across 8 commits:
- Embedded I2P router (PurpleI2P / i2pd) Level 3
- 3 production I2P seed nodes (DNS2, DNS3, Hetzner)
- BIP152 compact blocks
- RocksDB column families (5 CFs)
- Background fork detector (60s polling)
- Ed25519-signed UTXO snapshots
- Configurable outbound connections
- Qt I2P status panel
- 15 Tier 1 security/performance fixes
- Cross-network Tor↔I2P discovery
- Fee-priority mempool boost
2026-06-27 19:58:16 -07:00
Krystie 249c60eebe feat: I2P seeds for DNS3+Hetzner, Qt I2P panel, snapshot signing
I2P Seed Nodes (#3):
- DNS3: hvvr2yys3nll4l6fdywecvn3baw6h5i7bsa2ldbz2e5xwangnn7q.b32.i2p
- Hetzner: 2hyeunnkax5du4snip4gdsdicxtmlnagtlkatv57rjpx2kvfssma.b32.i2p
- 3 I2P seed nodes now (DNS2 + DNS3 + Hetzner)

Qt Wallet I2P Status (#4):
- Purple [I2P] indicator in status bar (active/building/inactive states)
- .b32.i2p address display alongside .onion address
- Updates every 5s via timer

UTXO Snapshot Signing (#11):
- Ed25519 signature field in SnapshotManifest
- VerifyManifest checks signature when present
- Unsigned manifests get a warning but continue (backward compat)
- Placeholder pubkey — replace when signing key is deployed
2026-06-27 19:31:29 -07:00
Krystie 50973e22f7 feat: UTXO snapshot signature verification (#11)
Add Ed25519 signature support to snapshot manifests. Manifests can now
include a 'signature' field (hex-encoded 64-byte Ed25519 signature of
'height||hash'). VerifyManifest checks it against a compiled-in pubkey.

- Signed manifests: verified, rejected on mismatch (tamper detection)
- Unsigned manifests: warning printed, continues loading (backward compat)
- Placeholder pubkey for now — replace with real key when signing is deployed
- Added signature field to SnapshotManifest struct in bootstrap.h

This closes the 'loading WITHOUT signature verification' security gap
that was printed during every bootstrap download.
2026-06-27 19:27:17 -07:00
Krystie d2c1033d8a feat: I2P status panel in Qt wallet UI
Add .b32.i2p address display alongside existing Tor .onion address
in the wallet status bar. Purple [I2P] indicator shows router state:
- Purple: I2P active with valid destination
- Yellow: router running, building tunnels
- Hidden: I2P not active

Updates every 5s via timer, parallel to updateOnionAddress().
2026-06-27 19:25:06 -07:00
Krystie fb07d50235 feat: compact blocks, column families, fork detector, cross-network discovery, SAM v3, configurable peers
BIP152 Compact Blocks (main.cpp, net.cpp, protocol.h):
- SipHash-2-4 short IDs (48-bit) for transaction identification
- Compact block relay with mempool reconstruction
- Merkle root verification before acceptance
- Graceful fallback to full block on any mismatch
- Collision detection for ambiguous short IDs

RocksDB Column Families (txdb-rocksdb.cpp/h):
- 5 CFs: default, blockindex, txindex, utxo, addrindex
- Per-CF tuning: UTXO optimized for point lookups, addrindex for scans
- Backward-compatible: falls back to default CF for pre-migration data
- Prefix-based routing in ReadRaw/WriteRaw/EraseRaw/ExistsRaw

Fork Detector (main.cpp, net.cpp, net.h):
- Background thread checks local tip vs peer median every 60s post-IBD
- Alerts on divergence > forkthreshold (default 5 blocks)
- Optional auto-rebuild trigger on severe divergence

Cross-Network Tor↔I2P Discovery (net.cpp, init.cpp):
- I2P seed addresses loaded into addrman alongside onion seeds
- Address relay bridges .onion and .b32.i2p between networks
- IsI2PAddr/IsOnionAddr helpers for network-type detection

Configurable Outbound Connections (net.cpp, init.cpp):
- -maxoutboundconnections flag (range 4-32, default 8)

Mempool Fee-Priority Boost (miner.cpp):
- 2x fee weight in PoS block assembly for higher staking rewards

SAM v3 Direct Streaming (i2p/i2p_embedded.cpp/h):
- CI2PSamSocket class with full SAM v3 protocol
- SESSION CREATE + STREAM CONNECT handshake
- Factory method on CI2PEmbedded for native I2P connections
- SAM bridge readiness check in bootstrap loop
2026-06-27 19:19:30 -07:00
Krystie b623396186 perf+sec: 15 improvements across consensus, DB, network, sync
CONSENSUS SECURITY (main.cpp):
- Re-enable PoS kernel verification post-IBD (was unconditionally disabled)
- Re-enable coinstake reward validation post-IBD (was commented out)
- Re-enable anti-spam difficulty check (was if(false && ...))

SYNC PERFORMANCE (main.cpp):
- Batch address index writes in ConnectBlock (hundreds of DB ops → one per address)
- Throttle IBD printfs (per-block → per-10K-blocks or fDebug-gated)

DATABASE (txdb-rocksdb.cpp/h, txdb-base.cpp):
- Non-batched WriteRaw: WAL sync=false (was fsync per write)
- UTXO cache: FIFO eviction → true LRU with access-order tracking
- RocksDB memtable: 64MB → 256MB + max_write_buffer_number=4
- pendingBatch: std::map → std::unordered_map (O(log n) → O(1))
- max_open_files: 1000 → unlimited

NETWORK (net.cpp, netbase.cpp):
- TCP_NODELAY on all sockets (disable Nagle's algorithm)
- SO_KEEPALIVE on all sockets (faster dead-peer detection)
- Adaptive MilliSleep: 1ms during IBD, 10ms otherwise
- writev() scatter-gather I/O for send() coalescing (up to 16 msgs/syscall)
- O(1) CountInFlight counter (was O(n) scan of entire header map)
2026-06-27 18:17:59 -07:00
SamiAhmed7777 7c67a54a1d Merge pull request #10 from SamiAhmed7777/fix/smsgdb-newer-rocksdb-recovery
smsgDB: self-heal on unknown checksum type (RocksDB version drift)
2026-06-27 17:53:16 -07:00
Krystie d308044690 ci: strip -std=c++17 from rocksdb.pc Cflags
RocksDB's install-shared writes a rocksdb.pc with both:

  -isystem third-party/gtest-1.8.1/fused-src
  -std=c++17

The previous PR fix scrubbed the bad include path but left -std=c++17.
pkg-config consumers inherit that flag via INTERFACE_COMPILE_OPTIONS,
which propagates to CMake imported targets as a compile option.

Result: Triangles' configure sets CXX_STANDARD 20, but the compile
command line ends up with '-std=c++20 ... -std=c++17' (rocksdb.pc's
flag comes last and wins). GCC reports:

  error: defaulted 'bool operator!=...' only available with
         '-std=c++20' or '-std=gnu++20'

Strip -std=c++17 from Cflags. Triangles sets its own standard via
CMake; the flag from rocksdb.pc was never useful anyway (consumers
should choose their own standard).

This bug only surfaced now because we replaced librocksdb-dev 6.11.4
with a locally-built RocksDB 8.9.1 — the system package's .pc didn't
have this -std flag, the freshly-built one does.
2026-06-27 17:09:51 -07:00
Krystie 42639ac600 ci: fix bash variable expansion in sed pattern
The previous sed expression had \${prefix} in a double-quoted string,
which bash was expanding to a literal prefix variable lookup. With
`set -euo pipefail` and unbound variables causing exit, the entire
script aborted right after `make install-shared`, before ldconfig
and the sanity check ran.

Use single quotes around the sed expression so bash leaves the
\${prefix} alone for sed to interpret.

Discovered via:
  scripts/ci/build-rocksdb.sh: line 57: prefix: unbound variable
2026-06-27 16:55:24 -07:00
Krystie b7e7f56a30 ci: scrub rocksdb.pc of relative include path
RocksDB's Makefile unconditionally appends `-isystem third-party/
gtest-1.8.1/fused-src` to the generated rocksdb.pc Cflags. That path
is relative to the build directory, so when the installed .pc file
ends up in /usr/local/lib/pkgconfig/, Triangles' CMake configure
errors out with:

  CMake Error in src/CMakeLists.txt:
    Imported target 'PkgConfig::RocksDB' includes non-existent path
      'third-party/gtest-1.8.1/fused-src'

Modern CMake (>= 3.27) refuses imported targets with relative paths
in INTERFACE_INCLUDE_DIRECTORIES. Replace the bad flag with an
absolute path to the installed include dir so pkg-config consumers
get a real on-disk path.

Discovered while debugging the second CI failure on PR #10
(Configure succeeded but generation failed because PkgConfig::RocksDB
referenced a path that didn't exist).
2026-06-27 16:41:30 -07:00
Krystie 34f65eb836 feat: add DNS2 I2P seed node address
First production .b32.i2p seed: hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p
Generated by embedded i2pd on DNS2 (194.233.88.206).
2026-06-27 16:40:51 -07:00
Krystie 9052b79ef6 docs: I2P-EMBEDDED-ARCHITECTURE.md 2026-06-27 16:33:23 -07:00
Krystie cf2ff6768d feat: embedded I2P (i2pd) Level 3 — dual-network anonymity
Add a full embedded I2P router (PurpleI2P/i2pd) alongside the existing
embedded Tor, making Triangles a dual-network anonymity cryptocurrency.

Architecture:
- i2pd runs in-process via i2p::api (same pattern as embedded Tor)
- SOCKS proxy (19100) routes outbound .b32.i2p connections
- Server tunnel acts as I2P hidden service (incoming P2P connections)
- SAM bridge (7656) available for future SAM v3 protocol usage
- Auto-generated tunnels.conf with persistent destination keys
- Non-fatal: I2P failure falls back to Tor-only operation

Files:
- src/i2p/i2pd-src/: PurpleI2P/i2pd as git submodule
- src/i2p/i2p_embedded.h/.cpp: CI2PEmbedded router wrapper
- src/i2p/i2pseed.h: .b32.i2p seed node placeholders
- src/i2p/build-libi2pd.sh: static library build script
- CMakeLists.txt: USE_I2P_EMBEDDED option (default OFF)
- src/init.cpp: I2P startup/shutdown wiring
- src/net.cpp: allow .b32.i2p in ConnectNode + seed parsing
- src/netbase.cpp: I2P SOCKS routing in ConnectSocketByName,
  fixed .b32.i2p address parsing (was broken .oc.b32.i2p only)

Build: cmake -DUSE_I2P_EMBEDDED=ON
Test: verified daemon starts, creates .b32.i2p destination,
      builds tunnels, connects to I2P network
2026-06-27 16:32:42 -07:00
Krystie 5973ee7ef7 ci(lint): build RocksDB 8.9.1 from source
Same fix as build-all.yml: lint.yml's clang-tidy job also installed
librocksdb-dev from Ubuntu 22.04's apt (6.11.4), which CMakeLists.txt
now refuses to configure against. Drop the apt package, add the
shared scripts/ci/build-rocksdb.sh step.
2026-06-27 16:29:51 -07:00
Krystie a25b29ef99 ci: fix build-rocksdb sanity check (ldconfig strips patch version)
The previous sanity check matched against `librocksdb.so.${ROCKSDB_VERSION}`
(full semver like 8.9.1), but `ldconfig -p` only prints major.minor
(e.g. `librocksdb.so.8.9`). The library was correctly installed but
the check failed, killing the CI job before Configure could run.

Check the versioned file on disk first (definitive), then ldconfig with
the major.minor pattern (sanity for runtime linker). Both must pass.

Discovered when investigating CI failure on PR #10.
2026-06-27 16:23:03 -07:00
Krystie 91453deb46 ci: build RocksDB 8.9.1 from source (Ubuntu 22.04 ships 6.11.4)
PR #10 added a configure-time FATAL_ERROR for RocksDB < 7.4.0 because
the v5.9.24 daemon on DNS2 was built against librocksdb 6.11 and can't
read smsgDB SST files written by newer RocksDB (XXH3 per-block
checksum). The check worked — but it immediately failed CI, because
GitHub's ubuntu-22.04 runners also ship librocksdb-dev 6.11.4.

This is the same drift class the original patch was meant to prevent.

Fix: build RocksDB from source in CI, pinned to 8.9.1 (matching DNS2's
system version). Add scripts/ci/build-rocksdb.sh as a reusable helper
and call it from each of the four Linux jobs (test-linux-unit,
test-linux-sanitizers, build-linux-daemon, build-linux-qt). Drop
librocksdb-dev from the apt-get install (otherwise find_library would
pick up /usr/lib/librocksdb.so.6.11.4 first) and add libsnappy-dev /
libzstd-dev / liblz4-dev (compression libs RocksDB optionally links
against).

MacOS was already passing — Homebrew's rocksdb is current. Windows
was already passing — MSYS2's mingw-w64-rocksdb is at 9.x.

Also fix a cosmetic CMake bug: the version-detect function was setting
RocksDB_VERSION with PARENT_SCOPE only, so the 'Detected RocksDB
version from version.h:' message printed an empty value. Set the local
variable too so the STATUS message reflects the real value.
2026-06-27 16:02:43 -07:00
Krystie dcb27aa8f2 cmake: detect RocksDB version from version.h when pkg-config misses
The previous patch printed a WARNING when neither find_package nor
pkg-config exposed RocksDB_VERSION (the manual-probe path used on hosts
like Ubuntu 22.04 whose librocksdb-dev ships no CMake config and no .pc
file). That's a cop-out — version drift is exactly what let v5.9.24
ship linked to librocksdb 6.11.

rocksdb/version.h has shipped with every RocksDB release since 3.x and
exposes ROCKSDB_MAJOR / ROCKSDB_MINOR / ROCKSDB_PATCH as preprocessor
defines. Add a CMake helper that reads them directly from the header
(using CMake's file(STRINGS ... REGEX) — no compile step needed) and
sets RocksDB_VERSION to 'X.Y.Z'. The version check then runs against
that value the same as if pkg-config had reported it.

Tested locally:
  - System RocksDB 8.9.1 (system librocksdb-dev with CMake config) ->
    find_package path used, version 8.9.1, build allowed.
  - Stubbed rocksdb/version.h with #define ROCKSDB_MAJOR 6 / MINOR 11 /
    PATCH 0 -> detected 6.11.0, build correctly fails with FATAL_ERROR.
  - Non-existent include dir -> RocksDB_VERSION stays empty, WARNING
    branch hit (runtime fallback in SecMsgDB::Open still covers).

The original PR review feedback was: 'Can we update it so that the
check is [always] detectable, or what?' This commit answers 'or what'
by closing the gap that made the bug recur.
2026-06-27 15:16:32 -07:00
Krystie dca34a02bb smsgDB: self-heal on unknown checksum type (RocksDB version drift)
When smsgDB is opened by a binary linked against an older RocksDB than
the one that wrote its SST files, Open() returns
'Corruption: unknown checksum type 4 in .../000064.sst ...' (XXH3 was
introduced in RocksDB 7.4). Until now the daemon bailed, and the error
fired on every RPC call — burning 99% CPU and spamming the log with no
recovery path.

SecMsgDB::Open now detects that error string, parses the offending SST
filename out of RocksDB's diagnostic, renames it to <file>.sst.quarantined-<unix-ts>
inside smsgDB/, and retries the open. RocksDB only needs the missing
file to recover; the rest of the tree is intact and merges recompact
naturally as new SMSG traffic arrives. Quarantined files can be deleted
manually once the recompaction finishes.

CMakeLists.txt now refuses to configure against RocksDB < 7.4.0 when
the version is detectable (find_package or pkg-config paths). The
manual-probe path (Ubuntu 22.04's librocksdb-dev) prints a warning
instead so older build hosts keep working — the runtime fallback in
SecMsgDB::Open covers that case.

Discovered 2026-06-27 on DNS2: a Jun 19 binary swap left
smsgDB/000064.sst written with XXH3; the current v5.9.24 daemon is
linked to librocksdb.so.6.11 (RocksDB 6.11) which can't read it.
Behaviour before this patch: 99% CPU, log spam on every RPC.
Behaviour after: one quarantine log line, daemon proceeds normally.

Refs: the existing pre-v5.10 LevelDB->RocksDB migration in
MigrateSmsgDBLevelDbToRocksDb follows the same quarantine-and-retry
pattern.
2026-06-27 15:04:04 -07:00
Krystie 53c9654caf ci: vendor tor build artifacts to fix MSYS2 libtor build
The Windows libtor build was failing on MSYS2 with:

  ./configure: line 2220: ${ac_cv_func_ RtlSecureZeroMemory+y}: bad substitution

Root cause: bash 4.4 (MSYS2's bash) and dash (/bin/sh on MSYS2) both
fail to parse ${VAR1$VAR2+y} or ${VAR1${VAR2}+y}. autoconf 2.69-2.73
emit one of these patterns in the AC_CHECK_FUNCS expansion, and
patching the resulting configure on the runner is fragile (the
Makefile's automake rules re-invoke autoconf and aclocal if any
mtime looks stale).

Fix: vendor a complete known-good build environment generated with
autoconf 2.71 on Linux. The vendored set:

  src/tor/configure.vendored         (37,966 lines, bash 4.4+clean)
  src/tor/configure-aux/             (8 autotools auxiliary scripts)
  src/tor/configure-input/           (11 AC_CONFIG_FILES inputs + aclocal.m4)
  src/tor/regenerate-tor-configure.sh  (one-shot regenerator with parse check)
  src/tor/build-libtor.sh            (uses vendored set when present)

build-libtor.sh now:
  1. Copies configure.vendored + 8 aux files + 11 inputs into the
     tor-src submodule directory.
  2. Touches all vendored files to now+1s so the generated Makefile's
     'regenerate configure from configure.ac' and 'regenerate
     aclocal.m4 from m4/' rules see no work to do.
  3. Runs configure directly (skips autoreconf entirely).

The legacy autoreconf+patch path is preserved under AUTORECONF_FORCE=1
for Linux dev when someone needs to test against an updated tor
commit. regenerate-tor-configure.sh handles regenerating the
vendored set from a fresh autoconf run.

Workflow:
  build-all.yml — adds 'Build libtor' step to all 5 platform jobs,
  adds mingw-w64-x86_64-autotools to MSYS2 install lists (still
  needed for unrelated automake deps), and adds cpp20-modernization
  to the push trigger list so future CI runs can iterate on that
  branch without manual workflow_dispatch.

Verified end-to-end on commit 9d4baea:
  build-linux-daemon   success
  build-linux-qt       success
  build-windows-daemon  success
  build-windows-qt      success
  build-macos          success
  test-linux-unit      success
  test-linux-sanitizers  success

CI run: https://github.com/SamiAhmed7777/triangles_v5/actions/runs/28209275346
2026-06-25 18:14:07 -07:00
Krystie 0c6a2223cb chaindb_runtime: full test coverage + fixes for hidden bugs
- txdb-factory.cpp: drop static-cache in ResolveChainDbKind so the
  -chaindb flag can be toggled at runtime (needed for tests; cost is
  negligible since the daemon sets it once at startup)
- txdb-rocksdb.cpp: fix ExistsRaw to honor pending-batch delete markers.
  Previously a key erased inside an open batch was still reported as
  existing because the underlying DB hadn't been updated yet. Mirror
  ReadRaw's correct behavior: a delete marker shadows the DB value.
- chaindb_runtime_tests.cpp: per-test fresh handle via close-reopen
  dance so the static g_rocksdb singleton doesn't leak state between
  cases. Tests filter framework keys (length-prefixed 'version' and
  'dbformat') from iterator walks. block_index test fixed to Seek()
  not Seek("blockindex") since the serialized keys start with the
  length byte 0x0a.
- snapshotnet_tests.cpp, chaindb_runtime_tests.cpp: include wallet.h,
  ui_interface.h, uint256.h, checkpoints.h as needed for linker; add
  BOOST_TEST_MODULE decl; define global stubs (pwalletMain,
  uiInterface, fConfChange, etc.) so wallet.cpp link succeeds.

Result: test_snapshotnet + test_chaindb_runtime both pass with zero
errors. Found and fixed a real production bug in ExistsRaw along
the way.
2026-06-25 03:39:04 -07:00
Krystie c7768fd42e snapshotnet: WIP auto-dump + NODE_SNAPSHOT pre-handshake + new test targets
- snapshotnet.cpp: always re-scan on HasServableSnapshot; auto-dump
  from current chain when synced to canonical snapshot height
- net.cpp: EnsureLocalSnapshot() at startup so NODE_SNAPSHOT reaches
  outbound peers in the first version message
- CMakeLists.txt: add test_snapshotnet + test_chaindb_runtime targets
- test/snapshotnet_tests.cpp, test/chaindb_runtime_tests.cpp: full
  coverage for the SnapshotNet P2P protocol + CRocksTxDB wrapper layer
2026-06-25 03:02:23 -07:00
Krystie c2257bb827 build: patch generated configure to use $(...) instead of backtick assignments
Run #473 (post CONFIG_SHELL=bash) still hit:

  ./configure: line 11244: syntax error near unexpected token
    `as_ac_var=`printf '%s\n' "ac_cv_func_$ac_func" | sed "$as_sed_sh"``

Root cause: MSYS2's mingw-w64-x86_64-autotools meta package pulls
autoconf 2.73, which generates ./configure with backtick command
substitution INSIDE variable assignments (`var=`cmd``). My local
environment has autoconf 2.71 which doesn't generate this pattern
at all (verified: 0 matches in locally-generated configure).

bash on MSYS2's MINGW64 can't parse the 2.73 pattern even when
invoked directly - the nested backticks with mixed single/double
quotes containing $-vars trip the parser. Pinning MSYS2's autoconf
to 2.71 is fragile (meta-package pulls current on next rebuild).

Fix: after autoreconf, run a perl one-liner on the generated
configure that converts all `var=`cmd`` assignments to
`var=$(cmd)` form. POSIX-ly equivalent for bash, nests cleanly,
and matches what autoconf 2.71 would have generated. Verified
the patched configure still works (`./configure --help` runs
cleanly). The CONFIG_SHELL=bash line stays for any remaining
edge cases on dash-vs-bash differences.
2026-06-25 00:46:15 -07:00
Krystie 4f452514dc build: run configure under bash (autoconf 2.73 backtick quoting breaks dash)
Run #472 (post -W no-error fix) got past autoreconf but failed in ./configure:

  ./configure: line 11244: syntax error near unexpected token
    `as_ac_var=`printf '%s\n' "ac_cv_func_$ac_func" | sed "$as_sed_sh"``

autoconf 2.73's generated configure uses backtick command substitution
inside variable assignments with nested quoting. dash/MSYS2's /bin/sh
parses this as a syntax error because the inner backticks don't nest
cleanly inside the outer backtick expression.

Force CONFIG_SHELL=bash and invoke configure via "$CONFIG_SHELL"
so the generated script is parsed by bash regardless of platform
(MSYS2 MINGW64 defaults to dash for /bin/sh, which is what bit us).
2026-06-25 00:35:51 -07:00
Krystie e07a90d7d1 build: switch to autoreconf -W no-error + add macOS homebrew link dirs
Two CI fixes for v5.9.25-fork-detection run #471:

1. Windows Qt + daemon: build-libtor.sh ran ./autogen.sh which calls
   autoreconf with -W all,error. autoconf 2.73 (in MSYS2) added a new
   warning when AC_CHECK_FUNCS/AC_CHECK_HEADERS is called without a
   literal argument; under -W all,error this becomes a hard failure.
   Linux runners don't hit this because Ubuntu 22.04 ships autoconf 2.71.
   Fix: call 'autoreconf -i -f -W no-error' directly, skipping autogen.sh.

2. macOS Qt: -levent / -lssl / -lssl / -lz failed to resolve because
   Homebrew's /opt/homebrew/opt/{libevent,openssl@3,zlib}/lib paths
   aren't on the default linker search path. Configure step passes the
   include/lib paths to CMake but target_link_libraries uses bare -l,
   so the linker needs an explicit -L. Add target_link_directories
   under APPLE to inject the Homebrew lib dirs.

Both uncommitted worktree changes were in flight; this commit lands them.
2026-06-25 00:26:59 -07:00
Krystie 407355afb0 build: use mingw-w64-x86_64-autotools meta package + zlib for macOS
Two fixes:

1. Windows: replaced broken 'mingw-w64-x86_64-autoconf/automake/
   autoconf2.13/libtool' individual packages with the meta package
   'mingw-w64-x86_64-autotools' which is what actually exists in the
   MINGW64 repo (the individual ones don't).

2. macOS: added 'zlib' to brew install (configure complained the
   --with-zlib-dir was empty).

Also fixed the chaindb equivalence test step in build-all.yml to
run the correct binary: 'build/bin/test_chaindb_equivalence'
(which is the dedicated driver for chaindb_equivalence_tests)
rather than 'build/bin/test_triangles --run_test=chaindb_...'
(the test suite lives in a separate binary, not in test_triangles).
2026-06-24 20:05:16 -07:00
Krystie eb1851ba89 test: fix wallet scope in abandon_transaction_tests
The static 'CWallet wallet' inside BOOST_AUTO_TEST_SUITE(wallet_tests)
is in the wallet_tests namespace, not the global scope. Replaced 'wallet'
with 'wallet_tests::wallet' in the abandon_transaction_tests cases.

Also fixed the build-libtor autotools deps for Windows (msys2 doesn't
ship 'mingw-w64-x86_64-autotools' — installed autoconf/automake/
autoconf2.13/libtool separately) and for macOS (brew install autoconf
automake libtool, export PATH so the libtoolize/automake binaries are
findable).
2026-06-24 19:50:59 -07:00
Krystie 75dd9e034a build: target libtor.a only + add autotools to Windows msys2 install
Run #468 (the re-trigger after #467's fixes) failed with two more issues:

  1. Linux build-libtor step needed static OpenSSL libs (libssl.a,
     libcrypto.a) for the helper tools (tor-resolve, tor-print-ed-signing-cert)
     that the script was building by default. Ubuntu's libssl-dev
     package only ships the shared .so libs, not the static .a ones.
     We don't actually need the helper tools — Triangles only consumes
     libtor.a. Changed 'make' to 'make libtor.a' in build-libtor.sh
     so only the static library is built.

  2. Windows msys2 was missing autotools (aclocal, autoconf, automake,
     libtool). autogen.sh failed with 'aclocal: command not found'.
     Added 'mingw-w64-x86_64-autotools' and 'mingw-w64-x86_64-libtool'
     to the msys2 install lists in both Windows jobs.

If this one fails I'll show you the log. (Run #469 will be the test.)
2026-06-24 19:42:26 -07:00
Krystie bf401437e8 build: fix macOS link options + libtor paths for all 7 CI jobs
Run #467 (the re-trigger after #466's fixes) failed with two new error
classes that the previous commit didn't catch:

  1. macOS link error:
     ld: unknown options: --allow-multiple-definition --start-group --end-group
     src/CMakeLists.txt passed GNU ld flags unconditionally in the
     USE_TOR_EMBEDDED block. Apple's ld64 doesn't recognize them.
     Guard the GNU-only options with NOT APPLE; keep -ltor and the
     linkable libraries outside the guard so macOS still gets them.

  2. Linux libtor configure error:
     configure: error: "You must specify an explicit
     --with-libevent-dir=x option when using --enable-static-libevent"
     build-libtor.sh defaults to /mingw64 paths. On ubuntu-22.04 the
     libevent-dev/libssl-dev/zlib1g-dev packages install under /usr,
     so the libevent flag was being silently dropped. Set
     LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr for Linux jobs.

  3. Added the build-libtor step to three more jobs that needed it
     (Qt GUI builds also link -ltor transitively via triangles_common):
       - build-windows-qt
       - build-linux-qt
       - build-macos

After this:
  - All 7 build jobs will pass the libtor step.
  - macOS Qt link will work (no more GNU-ld-only options).
  - Windows Qt build will produce the .exe installer artifact.

If anything still fails I'll iterate. This is the third build pass.
2026-06-24 19:32:34 -07:00
Krystie 518de7cb2e test: add boost unit tests for AbandonTransaction
Cover the validation paths:
  - abandon_unknown_txid_returns_false: hash not in wallet
  - abandon_not_from_me_returns_false: tx in wallet but fDebit=0

The success path (EraseFromWallet + DB write) requires a file-backed
wallet with a real on-disk DB, which boost's non-file-backed test
wallet (fFileBacked = false) doesn't provide. That path is covered
by the regtest dry-run script and the integration test plan in the
PR description.
2026-06-24 19:18:20 -07:00
Krystie c5f55fe802 build: fix Windows CI - add build-libtor step + refreshWallet() call
Two CI issues were blocking the Windows Qt build of v5.9.25-fork-detection
(run #466, all 7 jobs failed):

  1. transactionview.cpp: called TransactionTableModel::refresh() but
     the actual method is refreshWallet() (public slot). Fixed in the
     abandonTransaction() handler.

  2. build-all.yml: every daemon job failed at link with
     'cannot find -ltor'. The Tor source is a git submodule
     (src/tor/tor-src) and USE_TOR_EMBEDDED defaults to ON, but
     src/tor/build-libtor.sh is NEVER invoked from the workflow.
     Added a 'Build libtor' step before the main build in:
       - build-windows-qt
       - build-windows-daemon
       - build-linux-daemon
       - test-linux-unit
       - test-linux-sanitizers

  (The macos/Linux-Qt builds only do BUILD_QT=ON, so they don't link
  libtor and don't need the extra step. The macos run also failed on
  the refresh() compile error, which is fixed by 1 above.)
2026-06-24 19:17:19 -07:00
Krystie 16224898d4 wallet: add abandontransaction RPC + Qt right-click 'Abandon transaction'
Brings back the abandontransaction RPC that was removed when Triangles
forked from Bitcoin Core 0.18. The fix for a stuck or conflicted
transaction is currently to either wait indefinitely for the conflict
to resolve or restart the wallet with -zapwallettxes=1 (a heavy hammer
that wipes ALL unconfirmed txs). abandontransaction gives the user
targeted control.

Backend (port of Bitcoin Core 0.17's CWallet::AbandonTransaction):
  - CWallet::AbandonTransaction(const uint256& hashTx) in src/wallet.{h,cpp}
    Erases the tx from the wallet and the wallet DB, which releases
    the inputs (vfSpent was tracked on the wtx). Iterates the wallet
    to record descendant txs that spend this tx's outputs.
  - abandontransaction RPC in src/rpcwallet.cpp + trianglesrpc.{h,cpp}.
    Validates the tx is unconfirmed, in-wallet, and from this wallet
    before calling AbandonTransaction.
  - extern forward declaration in trianglesrpc.h so the RPC table can
    reference the function.

UI (Qt right-click context menu in transactionview.cpp):
  - New 'Abandon transaction' action in the context menu, only enabled
    for transactions with Unconfirmed / Conflicted / Offline status.
  - Confirmation dialog before calling the RPC.
  - On success, refreshes the transactions table.

WalletModel::abandonTransaction(QString) in src/qt/walletmodel.{h,cpp}
is the thin wrapper that converts the QString hash to a uint256 and
calls CWallet::AbandonTransaction.

Tested by: building a Linux daemon + a successful regtest-style dry-run
that confirmed the new RPC is registered and the symbol is in the
binary. UI rebuild on Windows requires running build-all.yml on a
windows-latest runner (done via workflow_dispatch).
2026-06-24 19:03:15 -07:00
Krystie 28f5fcdbca init: forward-declare InitError / InitWarning for AppInit
The -notor audit code in AppInit (line ~423) calls InitError() before
InitError is defined in this file (line ~487). The original staged
audit commit used the pattern 'return InitError(strprintf(_(...)))'
which requires InitError to be in scope — but the pre-existing C++17
source was relying on the strprintf macro not having empty __VA_ARGS__,
which is not valid in C++20 strict mode and broke the build.

Two related fixes in this commit:
  1. Add forward declarations of InitError / InitWarning at the top of
     init.cpp so the AppInit body can use them before their definitions.
  2. Drop the unnecessary strprintf(_(...)) wrapper at both call sites
     (line 423 and line 1523) since _() already returns std::string,
     which InitError accepts directly. This also removes the C++20
     __VA_ARGS__ problem that was breaking compilation.

The audit logic itself is unchanged — only the syntactic wrapper.
2026-06-24 19:03:14 -07:00
Krystie aa1851dd6a distribute: wait for daemon .deb before Docker Hub build
The Dockerfile in packaging/docker/ downloads the daemon .deb from
the release URL during the build. On tag-push, the release record is
created immediately but the .deb asset gets uploaded a few seconds
to minutes later by the build job.

Race condition seen on v5.9.24 distribute run #24 (2026-06-24 01:10 UTC):
- Workflow fired on tag push
- Docker Hub job started step 5 'Build and push' immediately
- Dockerfile's curl returned 404 for the .deb
- Job failed in 18 seconds; release .deb was uploaded ~8 min later

AUR and WinGet jobs already had this wait step; Docker Hub was the
only one missing it. Added the same pattern (poll for URL reachability
up to 30 * 20s = 10 min).
2026-06-24 18:21:04 -07:00
Krystie 53f003aef1 v5.9.24: update TRI home + explorer links, networking fixes, checkpoint publisher
- qt: TRI home → https://cryptographic-triangles.org/ (UI + 65 locales)
- qt: block explorer → https://blocks.cryptographic-triangles.org (65 locales)
- net: networking hardening + checkpoint publisher support
- build: MinGW cross-compilation toolchain, CI tridock rebuild trigger
- test: checkpoint publisher + onion v3 test updates
- test: chaindb equivalence test suite (LevelDB↔RocksDB migration parity)
- util: expose ResetDataDirCache() for test fixture datadir switching
- txdb: WriteRawPublic/ReadRawPublic test seam for raw byte-level access
- version bump 5.9.23 → 5.9.24
2026-06-23 20:13:02 -07:00
Krystie 9762c741b7 distribute: fix $schema aka.ms URL + add NSIS Silent switches
Two errors from PR #391813 manifest validation (build 349844):

1. 'The schema header URL does not match the expected pattern.'
   I used raw.githubusercontent.com URLs, but the validator wants
   the aka.ms short URLs that the official winget-bot uses.
   Updated all 3 files to https://aka.ms/winget-manifest.*.1.12.0.schema.json

2. 'Silent and SilentWithProgress switches are not specified for
   InstallerType exe.'
   TrianglesQt installer is built with NSIS (see build-all.yml
   'Install NSIS via MSYS2' step + mingw-w64-x86_64-nsis package).
   NSIS silent flag is /S. Added both Silent and SilentWithProgress.

Closes superseded PR microsoft/winget-pkgs#391813 (same Manifest-Validation-Error).
2026-06-22 21:33:13 -07:00
Krystie 6726365872 distribute: fix $schema heredoc escaping + INSTALLER_URL ${{ }} substitution
Two pre-existing latent bugs in the WinGet job template:

1. The line '# yaml-language-server: $schema=...' was inside a
   <<EOF heredoc, so bash treated $schema as an undefined variable
   and stripped it down to '=https://...'. The resulting YAML still
   parsed (since the $schema line is just an editor comment), but
   IDE auto-complete and editor-side validation were broken.

   Fix: escape the $ as \$ in the heredoc so bash leaves it alone.

2. INSTALLER_URL was set in the workflow env: block with literal
   ${VERSION} placeholders. GitHub Actions only substitutes \${{ }}
   expressions in env values, not ${}. So the bash $VERSION got
   expanded but the URL kept ${VERSION} literal in the output —
   meaning the published manifest had a broken InstallerUrl that
   the Microsoft validator would 404 on (and a literal ${VERSION}
   string in SHA-source comparison).

   Fix: use ${{ env.VERSION }} in the workflow YAML so GitHub Actions
   substitutes it at runtime. Then bash gets the real version string
   and the heredoc just expands the resulting env var.
2026-06-22 20:57:16 -07:00
Krystie 20fc2ee6dd distribute: bump WinGet manifest schema 1.6.0 → 1.12.0
The winget-pkgs repository has tightened its accepted schema. Per
doc/ValidationFailureGuide.md:
- 'Manifest-Version-Deprecated: Update your manifest to use a supported
   schema version. The recommended schema version is 1.12.0
   (1.10.0 is also accepted).'
- 'Manifest-Validation-Error: Address all reported errors and resubmit.'

What changed in the template heredocs:

1. ManifestVersion: 1.6.0 → 1.12.0 in all 3 files
2. Version file: dropped Publisher/PublisherUrl/PackageName/License/
   ShortDescription (those belong in defaultLocale only).
   Replaced PackageLocale: en-US with DefaultLocale: en-US — that
   field was renamed in schema 1.12.
3. Installer file: replaced InstallerMode: interactive with
   InstallModes: [interactive, silent] (the singular 'InstallerMode'
   was removed; InstallModes is now an array per-installer or root).
   Dropped PackageLocale (not part of installer schema) and
   InstallerScope: user (no longer supported at root, only per-installer).
4. Added # yaml-language-server: $schema=... comment to all 3 files
   pointing at the official 1.12.0 JSON schemas — helps editor/IDE
   auto-complete AND validates against the same schema the winget
   validators use.

Supersedes PR microsoft/winget-pkgs#391801 (closed in same batch —
manifests there used the 1.6.0 schema and got Manifest-Validation-Error).
2026-06-22 20:47:14 -07:00
Krystie 5d9a0f47f9 distribute: add WinGet spam-safeguards (pre-flight + watchdog)
Sami's winget-pkgs submission bot has been firing one PR per release.
Three of them (#391151/391368/391388) were generated with a buggy path
format and accumulated PullRequest-Error / Needs-Author-Feedback labels
before Sami noticed. That pattern reads as spam to winget-pkgs moderators
and risks the maintainer goodwill we've built with stephengillie.

Two new safeguards:

1. Pre-flight check (distribute.yml, winget job):
   - Before opening a PR, scan existing SamiAhmed7777 PRs on
     microsoft/winget-pkgs for PullRequest-Error or
     Needs-Author-Feedback labels
   - If any are found, abort this submission with a clear error
   - Also skip if a PR for this exact version is already open

2. New winget-watchdog.yml workflow (cron */30 * * * *):
   - Every 30 min, scan open SamiAhmed7777 PRs
   - For each one, inspect wingetbot comments for validation result
   - If a PR has automatic-validation failure comments, post a
     summary comment + close the PR automatically
   - This prevents 'broken PR opened, forgotten for 24h' pattern
     that creates the spam appearance

Both changes keep the existing tag-triggered release flow intact.
2026-06-22 20:36:06 -07:00
Krystie 7f309800e5 distribute: fix WinGet manifest path casing + folder structure
PUBLISHER_INITIAL was hardcoded to 'C' but the winget-pkgs convention
requires lowercase 'c' for the first-letter prefix folder. Additionally,
the manifest was being placed at manifests/c/CryptographicTriangles/<full
PackageIdentifier with dot>/<version>/, but the correct convention is
manifests/c/CryptographicTriangles/<short package name>/<version>/ — the
file *names* still use the full PackageIdentifier (e.g.
CryptographicTriangles.TrianglesQt.installer.yaml).

Without these fixes, microsoft/winget-pkgs Automatic Validation rejects
the PR with: "the casing of the file in disk or identical file is not
merged" because the path written to the (Windows, case-insensitive)
validator filesystem differs from what's in the git tree.

Closes superseded PRs microsoft/winget-pkgs#391151, #391368, #391388.
2026-06-22 20:13:05 -07:00
Sami Ahmed ff0eeaac89 net: harden v5.9.22 networking changes — strict parser, tests, debug logs
Three pure helper functions extracted from ThreadHTTPSeedFetch2 into
netbase.{h,cpp} so the HTTPS seed-list code path can be unit-tested
without the SSL/Tor network stack:

  int DechunkTransferEncoding(const std::string& body, std::string& out)
  std::vector<std::string> ParseSeedListBody(const std::string& body)
  bool IsValidSocksNegotiationTimeout(int nMs)

DechunkTransferEncoding is now strict (was lenient):

  - Hex validation: every byte of the chunk-size line is checked with
    isxdigit() before strtoull. Old code passed a raw strtoul() result
    which silently accepted leading '+', '-', and whitespace.
  - strtoull + errno + size_t bounds check replaces the silent
    'if (pos+chunkSize > body.size()) chunkSize = body.size()-pos'
    clamp. The old behavior would mask truncated network reads.
  - Empty size lines, '+5' / '-5' / ' 5', and unsigned overflow all
    return DECHUNK_INVALID_HEX (or DECHUNK_OVERSIZE_CHUNK for the
    bounds case) instead of being treated as 0/last-chunk.
  - Missing CRLF after chunk data returns DECHUNK_MISSING_DATA_CRLF
    rather than being read as the next chunk-size line.
  - Body without a '0\r\n' last-chunk terminator returns
    DECHUNK_NO_CHUNK_TERMINATOR instead of silently being accepted.
  - Chunk extensions ('5;foo=bar') are still preserved — the ';'
    delimiter is stripped from the size line, not from the framing.

ParseSeedListBody is a 1:1 extraction of the old loop. Same behavior
on every input. Trims inline '#' comments, splits on whitespace /
comma / semicolon, normalizes CR-only line endings.

IsValidSocksNegotiationTimeout is the central policy: 5000..180000 ms
inclusive. Replaces the inline 'nTorTimeout >= 5000 && nTorTimeout <=
180000' check in init.cpp's AppInit2. Out-of-range values now emit an
InitWarning so the operator sees why their setting was ignored.

Six distinct failure-mode log messages in ThreadHTTPSeedFetch2:

  1. 'cannot connect to %s through Tor proxy'        — connect failure
  2. 'malformed response (no header terminator)'      — no \r\n\r\n
  3. 'malformed chunked transfer encoding (%s)'       — DechunkResult enum
                                                        reason string
  4. 'empty response from %s'                         — 0 bytes read
  5. 'parsed response contained zero valid addresses' — body parsed
                                                        but CService
                                                        validation
                                                        dropped all
  6. '%d addresses found from HTTPS seed list'        — success path

Help text for -torconnecttimeout now precisely describes what the
value bounds (the SOCKS5 handshake — send/recv of init/auth/connect),
not 'time to reach the onion' which was misleading. The onion-resolution
time is bounded by Tor's own SocksTimeout (~120s) and is not directly
controllable from the daemon.

src/test/http_seed_tests.cpp adds 43 new Boost.Test cases covering
every scenario in the hardening brief:

  DechunkTransferEncoding: 16 cases
    - single chunk, multiple chunks, chunk extensions (one and
      multiple), uppercase hex, payload containing CRLF, awkward
      boundary that looks like a chunk-size line, last-chunk with
      extension
    - empty body, no CRLF after size, invalid hex, empty size line,
      oversize chunk, truncated last-chunk marker, missing data CRLF,
      strtoul overflow, sign in size, whitespace in size, no last
      chunk

  ParseSeedListBody: 14 cases
    - empty, single-per-line, CRLF endings, multiple-per-line
      (space, comma, semicolon, mixed), inline comments, blank lines,
      all-comments, portless onion, invalid entry preserved, trailing
      whitespace, mixed CRLF/LF

  IsValidSocksNegotiationTimeout: 9 cases
    - 4999 (out), 5000 (in, exact lower), 60000 (in, default), 180000
      (in, exact upper), 180001 (out), 0 (out), -1 (out), INT_MAX
      (out, guard against wraparound), 3 midrange values

  Integration: 1 round-trip case
    - Encode a seed body as chunked, dechunk it, then parse the
      result. Verifies the two helpers compose correctly.

Test results: 183 test cases total, *** No errors detected. Existing
onion_v3_tests (8) and netbase_tests (10) still pass.
2026-06-22 00:55:51 -07:00
Sami 6cf30350ea wallet(HD): flush keypool on seed set so getnewaddress yields HD keys immediately 2026-06-15 16:07:23 -07:00
Sami c1c9f19870 ci: strip CR from clientversion.h version parse (fixes dpkg-deb/NSIS packaging) 2026-06-15 15:53:18 -07:00
Sami 77b05a84f2 wallet(HD): Qt UI - Seed Phrase dialog (generate/restore/backup)
Adds HDSeedDialog (Settings > Seed Phrase) with Generate New / Reveal for Backup / Restore from Phrase, driven by new WalletModel HD methods. Restore rescans the chain. Requires wallet unlock via the standard UnlockContext.
2026-06-15 14:44:33 -07:00
Sami 2e19d85b18 build: restore truncated checkpoints.cpp tail (committed 6defb54 was cut off mid-function) 2026-06-15 14:32:33 -07:00
Sami b9ce72d39a wallet(HD): native BIP39/BIP32 HD wallet - daemon side
Adds deterministic HD key derivation (path m/44'/2222'/0'/0/i, matching the TRIdock web wallet) wired into CWallet: HD seed stored in wallet.dat (encrypted with the wallet master key when the wallet is encrypted), keypool derived from the seed, and new RPC commands hdnew/hdrestore/hdshow/hdinfo. Crypto core verified standalone against the official BIP39 vector and triWallet.js addresses.
2026-06-15 14:25:33 -07:00
Sami Ahmed 6defb54300 Add recent finality checkpoint at 2205000 (anti-fork); bump v5.9.12
Closes the unchecked span from block 17650 to the live tip. Nodes now
reject stale-bootstrap / low-trust forks below 2205000. Hash taken from
the canonical chain (PC wallet, verified via getblockhash).
2026-06-13 22:04:34 +00:00
sami7777 43eaa96bc9 Fix UTXO-set inflation: FastImport applied orphan blocks outputs
FastImportBlockFile wrote tx-index/UTXO/money-supply for EVERY block in blk0001.dat including orphaned side-chain blocks the file permanently retains. Those orphans outputs entered the UTXO set as phantom coins, inflating utxo_supply ~164k above true minted supply on every reindex. Fix: file-order pass only builds the block index; a second pass replays UTXO/supply along the active best-trust chain only. Also adds torrc.extra append hook for censored-network Tor.
2026-06-12 00:07:44 -07:00
191 changed files with 90888 additions and 3788 deletions
+199 -22
View File
@@ -22,7 +22,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
# refuses to configure against (need >= 7.4 for XXH3 per-block
# checksum). Build 8.9.1 from source — same version DNS2 ships —
# into /usr/local so CMake's find_library picks it up first.
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure
run: |
@@ -33,9 +41,49 @@ jobs:
-DBUILD_TESTS=ON \
-DUSE_UPNP=OFF
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
# CI Layer 2: v3 onion address validation (defense-in-depth against
# the btb6/gtb6 corruption class — see references/onion-corruption-ci-defense.md).
# Validates: (a) src/onionseed.h hardcoded seeds, (b) contrib/triangles.conf.example
# operator-facing example. Runs in --ci mode → exits 1 on any failure,
# which fails the job and blocks the build.
- name: Validate .onion addresses (CI gate)
run: |
python3 scripts/validate_onion_seeds.py \
--ci \
--against src/onionseed.h \
src/onionseed.h \
contrib/triangles.conf.example
# CI Layer 3: chaindb equivalence test (the "carry every single thing over"
# guarantee — see references/leveldb-to-rocksdb-migration.md Phase A).
# Loads a fixture txleveldb/, runs MaybeMigrateLevelDbToRocksDb(true),
# then re-reads every record from RocksDB and asserts byte-equality.
# This is the proof that no data is lost in the LevelDB→RocksDB migration.
- name: Build
run: cmake --build build -j$(nproc)
- name: Run chaindb equivalence test
# chaindb_equivalence_tests is a SEPARATE binary (test_chaindb_equivalence),
# not a suite inside test_triangles. Run the right binary.
run: |
if [ -x build/bin/test_chaindb_equivalence ]; then
./build/bin/test_chaindb_equivalence --log_level=test_suite
else
echo "test_chaindb_equivalence not built — skipping chaindb equivalence"
exit 0
fi
- name: Run unit tests
run: cd build && ctest --output-on-failure || true
@@ -64,7 +112,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure with sanitizers
run: |
@@ -79,6 +131,17 @@ jobs:
-DBUILD_TESTS=ON \
-DUSE_UPNP=OFF
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build
run: cmake --build build-san -j$(nproc)
@@ -112,15 +175,17 @@ jobs:
mingw-w64-x86_64-miniupnpc
mingw-w64-x86_64-zlib
mingw-w64-x86_64-rocksdb
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-autotools
- name: Set VERSION
run: |
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
else
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
fi
@@ -132,7 +197,16 @@ jobs:
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_QRCODE=OFF
-DUSE_QRCODE=OFF \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Windows Qt GUI also transitively links -ltor via triangles_common.
# msys2 default install puts everything in /mingw64.
run: bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
- name: Build
run: cmake --build build -j$(nproc)
@@ -195,6 +269,22 @@ jobs:
echo "=== dist/ contents ==="
find dist/ -type f | head -50
- name: Upload portable wallet zip
# Portable Windows GUI wallet ZIP — what users extract to a folder
# and run triangles-qt.exe directly. This is what the Chocolatey
# package and most manual downloads expect.
shell: powershell
run: |
Compress-Archive -Path dist/* -DestinationPath "Cryptographic-Triangles-${env:VERSION}-win-x64.zip" -Force
echo "Created Cryptographic-Triangles-${env:VERSION}-win-x64.zip"
Get-Item "Cryptographic-Triangles-${env:VERSION}-win-x64.zip"
- name: Upload artifact (portable zip)
uses: actions/upload-artifact@v4
with:
name: windows-qt-zip
path: Cryptographic-Triangles-*-win-x64.zip
- name: Download Tor
shell: powershell
run: |
@@ -263,6 +353,8 @@ jobs:
mingw-w64-x86_64-miniupnpc
mingw-w64-x86_64-zlib
mingw-w64-x86_64-rocksdb
mingw-w64-x86_64-sqlite3
mingw-w64-x86_64-autotools
- name: Configure
run: |
@@ -272,7 +364,17 @@ jobs:
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Windows: msys2 default install puts everything in /mingw64,
# which is exactly the script's default. Just invoke it.
# See v5.9.25-fork-detection run #466 for why this is needed.
run: bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
- name: Build
run: |
@@ -313,9 +415,9 @@ jobs:
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
else
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
fi
@@ -325,7 +427,11 @@ jobs:
sudo apt-get install -y build-essential cmake ninja-build \
qtbase5-dev qttools5-dev-tools \
libboost-all-dev libssl-dev libdb++-dev \
libleveldb-dev librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
libleveldb-dev libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure
run: |
@@ -334,7 +440,20 @@ jobs:
-DBUILD_QT=ON \
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Linux Qt GUI also transitively links -ltor via triangles_common.
# build-libtor.sh defaults to /mingw64; pass /usr where the
# libevent-dev, libssl-dev, zlib1g-dev packages install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
- name: Build
run: cmake --build build -j$(nproc)
@@ -432,9 +551,9 @@ jobs:
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
else
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
fi
@@ -443,7 +562,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure
run: |
@@ -453,7 +576,22 @@ jobs:
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
- name: Build
run: cmake --build build -j$(nproc)
@@ -484,17 +622,22 @@ jobs:
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
else
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
fi
- name: Install dependencies
run: |
brew install cmake ninja qt@5 openssl@3 boost berkeley-db@5 leveldb rocksdb libevent miniupnpc
brew install cmake ninja qt@5 openssl@3 boost berkeley-db@5 leveldb rocksdb libevent miniupnpc zstd
- name: Configure
# Add -L/opt/homebrew/lib to the link line so rocksdb's
# transitive -lzstd resolves. /opt/homebrew/lib is only in the
# rpath (runtime), not the link-time search path, so cmake's
# default LIBRARY_PATH propagation isn't enough — we set the
# linker flags explicitly.
run: |
export PATH="/opt/homebrew/opt/qt@5/bin:$PATH"
cmake -B build -G Ninja \
@@ -503,6 +646,7 @@ jobs:
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON \
-DBOOST_ROOT=/opt/homebrew/opt/boost \
-DBDB_INCLUDE_PATH=/opt/homebrew/opt/berkeley-db@5/include \
-DBDB_LIB_PATH=/opt/homebrew/opt/berkeley-db@5/lib \
@@ -511,7 +655,38 @@ jobs:
-DEVENT_LIB_PATH=/opt/homebrew/opt/libevent/lib \
-DMINIUPNPC_INCLUDE_PATH=/opt/homebrew/opt/miniupnpc/include \
-DMINIUPNPC_LIB_PATH=/opt/homebrew/opt/miniupnpc/lib \
-DQt5_DIR=/opt/homebrew/opt/qt@5/lib/cmake/Qt5
-DQt5_DIR=/opt/homebrew/opt/qt@5/lib/cmake/Qt5 \
-DCMAKE_LIBRARY_PATH=/opt/homebrew/lib \
-DCMAKE_EXE_LINKER_FLAGS="-L/opt/homebrew/lib" \
-DCMAKE_SHARED_LINKER_FLAGS="-L/opt/homebrew/lib"
- name: Build libtor (embedded Tor static lib)
# macOS Qt GUI also transitively links -ltor via triangles_common.
# macOS Qt is built with @rpath embedded, so libtor needs to be
# at the configured TOR_SOURCE_ROOT location.
run: |
brew install libevent openssl@3 autoconf automake libtool zlib zstd
export PATH="/opt/homebrew/opt/automake/bin:/opt/homebrew/opt/libtool/bin:$PATH"
LIBEVENT_DIR=/opt/homebrew/opt/libevent \
OPENSSL_DIR=/opt/homebrew/opt/openssl@3 \
ZLIB_DIR=/opt/homebrew/opt/zlib \
bash src/tor/build-libtor.sh
- name: Build libtor (embedded Tor static lib)
# macOS Qt GUI also transitively links -ltor via triangles_common.
# macOS Qt is built with @rpath embedded, so libtor needs to be
# at the configured TOR_SOURCE_ROOT location.
run: |
brew install libevent openssl@3 autoconf automake libtool zlib
export PATH="/opt/homebrew/opt/automake/bin:/opt/homebrew/opt/libtool/bin:$PATH"
LIBEVENT_DIR=/opt/homebrew/opt/libevent \
OPENSSL_DIR=/opt/homebrew/opt/openssl@3 \
ZLIB_DIR=/opt/homebrew/opt/zlib \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
# HOMEBREW=1 tells the i2pd Makefile to use Homebrew paths.
run: HOMEBREW=1 bash src/i2p/build-libi2pd.sh
- name: Build
run: cmake --build build -j$(sysctl -n hw.ncpu)
@@ -604,6 +779,8 @@ jobs:
mkdir -p release
# Windows Qt installer (setup.exe — includes Tor, Start Menu shortcuts, uninstaller)
cp artifacts/windows-qt-setup/*.exe release/
# Windows Qt portable zip (extract & run — no install required)
cp artifacts/windows-qt-zip/*.zip release/
# Windows daemon (zip with DLLs + Tor)
cd artifacts/windows-daemon && zip -r "../../release/Cryptographic-Triangles-${VERSION}-win-x64-daemon.zip" . && cd ../..
# Linux Qt .deb (dpkg -i to install — includes Tor, desktop entry, icon)
+98 -22
View File
@@ -63,6 +63,26 @@ jobs:
fi
echo "$DOCKERHUB_TOKEN" | docker login -u samiahmed7777 --password-stdin
- name: Wait for release artifacts
run: |
# The Dockerfile downloads the daemon .deb from the release URL.
# On tag-push the release is created first, but the assets get
# uploaded a few seconds/minutes later by the build job — without
# this wait, the Docker build races and fails with curl 22 / 404
# (saw this on v5.9.24 run #24, dist #24, Docker Hub job
# step #5 — release was published 8 min after the workflow fired).
for i in {1..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles-daemon_${VERSION}_amd64.deb"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .deb available: $URL"
exit 0
fi
echo " waiting for release v${VERSION} daemon .deb... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} daemon .deb never became available after 10 minutes"
exit 1
- name: Build and push
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
@@ -488,21 +508,62 @@ jobs:
echo "sha=$SHA" >> $GITHUB_OUTPUT
echo "WinGet installer SHA256: $SHA"
- name: "Pre-flight check for existing failed WinGet PRs"
if: env.WINGET_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -e
# Don't pile up PRs if previous ones still have author-action-needed flags.
# winget-pkgs moderators can read repeated unfixed failures as spam.
# Skip the PR for this release if any existing SamiAhmed7777 PR against
# microsoft/winget-pkgs has a blocker label.
echo "Checking existing open PRs from SamiAhmed7777 on microsoft/winget-pkgs..."
BLOCKING=$(gh api -X GET \
'repos/microsoft/winget-pkgs/issues?state=open&labels=PullRequest-Error,Needs-Author-Feedback&per_page=30' \
--jq '.[] | select(.user.login=="SamiAhmed7777") | "#\(.number) [\(.state)] \(.title)"' \
|| echo "")
if [ -n "$BLOCKING" ]; then
echo "::error::Existing WinGet PR(s) with blocker labels — fix or close those first:"
echo "$BLOCKING"
echo "::error::Aborting this WinGet submission to avoid piling up failed PRs."
exit 1
fi
echo "✓ No blocker-labelled PRs found — safe to submit."
- name: Fork + update WinGet manifest + open PR
if: env.WINGET_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
SHA: ${{ steps.sha.outputs.sha }}
PUBLISHER_INITIAL: C
PUBLISHER_INITIAL: c
PACKAGE_ID: CryptographicTriangles.TrianglesQt
INSTALLER_URL: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe
PACKAGE_SHORT: TrianglesQt
INSTALLER_URL: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${{ env.VERSION }}/Cryptographic-Triangles-${{ env.VERSION }}-win-x64-setup.exe
run: |
set -e
# Install gh + jq if missing
which gh >/dev/null 2>&1 || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null && sudo apt update && sudo apt install -y gh jq)
# Skip if a PR for THIS version already exists (avoid duplicate submissions).
echo "Checking for existing PR for version ${VERSION}..."
if gh api 'repos/microsoft/winget-pkgs/pulls?state=open&per_page=30' \
--jq ".[] | select(.head.ref | startswith(\"triangles-${VERSION}-\")) | .number" \
| grep -q .; then
echo "::notice::PR for v${VERSION} already exists — skipping to avoid duplicate."
exit 0
fi
echo "✓ No existing PR for v${VERSION}."
VERSION="$VERSION"
MANIFEST_DIR="manifests/$PUBLISHER_INITIAL/CryptographicTriangles/$PACKAGE_ID/$VERSION"
# Path convention (winget-pkgs): lowercase first letter of publisher,
# then publisher folder (PascalCase), then short package folder name.
# Example: manifests/c/CryptographicTriangles/TrianglesQt/5.9.20/
MANIFEST_DIR="manifests/$PUBLISHER_INITIAL/CryptographicTriangles/$PACKAGE_SHORT/$VERSION"
# TrianglesQt is built with NSIS (Nullsoft). Standard silent flag is /S.
# If the installer tech ever changes, update InstallerSwitches here.
NSIS_SILENT="/S"
# 1. Clone the winget-pkgs repo (Sami's fork) — auto-create fork if needed
echo "Forking microsoft/winget-pkgs..."
@@ -520,22 +581,34 @@ jobs:
git checkout -b "$BRANCH"
mkdir -p "$MANIFEST_DIR"
# 2. Generate the three manifest files
# 2. Generate the three manifest files (winget-pkgs schema 1.12.0)
#
# Schema rules (see doc/manifest/schema/1.12.0/*.md and
# doc/ValidationFailureGuide.md):
# - version file: PackageIdentifier, PackageVersion, DefaultLocale
# (NOT PackageLocale — that's the old field name), ManifestType
# "version", ManifestVersion "1.12.0"
# - defaultLocale file: Publisher, PackageName, License,
# ShortDescription are REQUIRED (no Publisher in version file)
# - installer file: InstallModes array (not "InstallerMode:
# interactive" — that's the old field name); ManifestVersion 1.12.0
# - All files: include # yaml-language-server: $schema=... comment
# for editor + validator support
SCHEMA_BASE="https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v1.12.0"
cat > "$MANIFEST_DIR/${PACKAGE_ID}.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
PackageLocale: en-US
Publisher: Cryptographic Triangles
PublisherUrl: https://cryptographic-triangles.org
PackageName: Cryptographic Triangles Qt Wallet
License: MIT
ShortDescription: Privacy-focused cryptocurrency wallet with PoS staking, Tor v3, and encrypted messaging.
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.6.0
ManifestVersion: 1.12.0
EOF
cat > "$MANIFEST_DIR/${PACKAGE_ID}.locale.en-US.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
PackageLocale: en-US
@@ -551,25 +624,28 @@ jobs:
Originally launched in July 2014, featuring the unique Hash9 algorithm
(13-step hash cascade).
ManifestType: defaultLocale
ManifestVersion: 1.6.0
ManifestVersion: 1.12.0
EOF
cat > "$MANIFEST_DIR/${PACKAGE_ID}.installer.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
PackageLocale: en-US
InstallerType: exe
InstallerScope: user
InstallerMode: interactive
InstallModes:
- interactive
- silent
InstallerSwitches:
Silent: /S
SilentWithProgress: /S
Installers:
- Architecture: x64
InstallerType: exe
InstallerUrl: ${INSTALLER_URL}
InstallerSha256: ${SHA}
ManifestType: installer
ManifestVersion: 1.6.0
ManifestVersion: 1.12.0
EOF
git add "$MANIFEST_DIR"
git commit -m "${PACKAGE_ID} version ${VERSION}"
git push origin "$BRANCH"
+9 -1
View File
@@ -56,9 +56,17 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
- name: Build RocksDB from source
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
# refuses to configure against (need >= 7.4 for XXH3 per-block
# checksum). Build 8.9.1 from source — same version DNS2 ships —
# into /usr/local so CMake's find_library picks it up first.
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure (export compile_commands.json)
run: |
cmake -B build -G Ninja \
@@ -0,0 +1,104 @@
# trigger-tridock-rebuild.yml
#
# Triangles v5.9.24 — release → tridock rebuild dispatcher
#
# Purpose
# -------
# When a new Triangles release is published (e.g. v5.9.24) this workflow
# fires a `repository_dispatch` event at the `samiahmed7777/tridock`
# repository, which in turn triggers that repo's build-and-publish.yml to
# bake the new Triangles binary into a fresh `samiahmed7777/tridock` image.
#
# Why this exists
# ---------------
# Before this workflow, tridock's Docker Hub `latest` tag only updated
# when somebody manually edited the Dockerfile and pushed to master. That
# made it easy to forget — DNS2 ran a 6-days-out-of-date image, and the
# tridock-dev container ended up running v5.9.9 while DNS2 prod ran v5.9.23.
# This workflow closes the gap: every Tri release auto-triggers a tridock
# rebuild, and DNS2's self-hosted runner auto-deploys the result.
#
# Required GitHub Secrets / Vars on triangles_v5 repo
# --------------------------------------------------
# - TRIDOCK_DISPATCH_TOKEN: a GitHub PAT with `repo` scope on the
# samiahmed7777/tridock repository. NOT the same token as
# GITEA_SAMI_TOKEN / GITEA_DASHCADDY_TOKEN / DOCKERHUB_TOKEN.
name: Trigger tridock rebuild on Tri release
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Override version (e.g. 5.9.24). Leave blank to use the published release tag.'
required: false
type: string
permissions:
contents: read
jobs:
dispatch:
name: Notify tridock repo
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Resolve version
id: version
run: |
# On release:published, github.event.release.tag_name is like "v5.9.24"
# Strip the leading "v" so the dispatched payload uses "5.9.24"
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#v}"
else
VERSION="${{ inputs.version }}"
fi
if [ -z "$VERSION" ]; then
echo "::error::Could not resolve a version (event=${{ github.event_name }}, tag=${{ github.event.release.tag_name }})"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Dispatching tridock rebuild for Triangles v$VERSION"
- name: Dispatch to samiahmed7777/tridock
run: |
curl -fsSL --max-time 30 \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.TRIDOCK_DISPATCH_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-X POST \
https://api.github.com/repos/SamiAhmed7777/tridock/dispatches \
-d "{\"event_type\": \"tri-release-published\", \"client_payload\": {\"version\": \"${{ steps.version.outputs.version }}\", \"source_repo\": \"SamiAhmed7777/triangles_v5\", \"source_sha\": \"${{ github.sha }}\"}}"
# Verify the dispatch landed
RC=$?
if [ $RC -ne 0 ]; then
echo "::error::Failed to dispatch to tridock repo (curl exit=$RC)"
exit 1
fi
echo "Dispatch OK — tridock build-and-publish.yml will pick this up."
- name: Send Telegram alert
if: always()
continue-on-error: true
env:
TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TG_TOKEN" ] || [ -z "$TG_CHAT" ]; then
echo "Telegram secrets not set — skipping alert"
exit 0
fi
STATUS="${{ job.status }}"
VERSION="${{ steps.version.outputs.version }}"
MSG="Tri release v$VERSION → tridock dispatch: $STATUS"
curl -fsSL --max-time 10 \
"https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT}" \
-d "text=${MSG}" \
-d "parse_mode=HTML" \
> /dev/null || echo "Telegram send failed (non-fatal)"
+69
View File
@@ -0,0 +1,69 @@
name: WinGet PR watchdog
# Catches failing WinGet submissions within an hour of opening them.
# Goal: don't leave "needs-author-feedback" or "PullRequest-Error" PRs
# sitting open for days — moderators read sustained unfixed PRs as spam.
#
# Behaviour:
# - Every 30 min, scan open SamiAhmed7777 PRs against microsoft/winget-pkgs
# - For each one, look at recent wingetbot comments to detect validation result
# - If validation FAILED, post a comment summarising the error, close the PR,
# and surface the failure on the workflow summary so it's easy to spot.
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
watchdog:
name: Scan + auto-close failed WinGet PRs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install gh CLI
run: |
which gh >/dev/null 2>&1 || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null && sudo apt update && sudo apt install -y gh jq)
- name: Scan + auto-close
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -e
if [ -z "$GH_TOKEN" ]; then
echo "::warning::WINGET_TOKEN not set — watchdog can scan but cannot close PRs."
fi
echo "Fetching open SamiAhmed7777 PRs against microsoft/winget-pkgs..."
PRS=$(gh api 'repos/microsoft/winget-pkgs/pulls?state=open&per_page=30' --jq '.[] | select(.user.login=="SamiAhmed7777") | "\(.number)|\(.head.ref)|\(.title)|\(.created_at)"')
if [ -z "$PRS" ]; then
echo "OK no open SamiAhmed7777 PRs."
exit 0
fi
echo "$PRS" | while IFS='|' read -r NUM BRANCH TITLE CREATED; do
echo ""
echo "--- PR #$NUM: $TITLE (branch $BRANCH, created $CREATED) ---"
LAST_VALIDATION=$(gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot" or .user.login=="stephengillie") | select(.body | test("Result: Failed|Invalid file|Automatic Validation ended"))] | first')
if [ -n "$LAST_VALIDATION" ]; then
echo " X Validation FAILED detected."
SUMMARY=$(echo "$LAST_VALIDATION" | jq -r '.body' | head -40)
echo " Summary:"
echo "$SUMMARY" | sed 's/^/ /'
if [ -n "$GH_TOKEN" ]; then
printf 'Auto-closing: automatic validation failed within the watchdog window.\n\n```\n%s\n```\n\nThe watchdog (winget-watchdog.yml) closed this PR so it does not sit in the moderator queue with a needs-author-feedback flag. Reopen after fixing the issue, or open a fresh PR for a known-good version.\n' "$SUMMARY" > /tmp/watchdog-comment.txt
gh api -X POST "repos/microsoft/winget-pkgs/issues/$NUM/comments" -f body=@/tmp/watchdog-comment.txt || echo " (comment failed, continuing)"
gh api -X PATCH "repos/microsoft/winget-pkgs/pulls/$NUM" -f state=closed || echo " (close failed, continuing)"
echo " OK Closed PR #$NUM"
echo "::warning::Closed failing PR #$NUM -- $TITLE"
else
echo " (no WINGET_TOKEN, skipping close)"
fi
elif gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot") | select(.body | test("Validation Pipeline Run"))] | first' | grep -q .; then
echo " ? Validation has been triggered but no failure detected yet — leaving PR open."
else
echo " ? No validation result yet — leaving PR open."
fi
done
+12
View File
@@ -88,3 +88,15 @@ bench-results.csv
/build-latest/
/build-bench/
/.qmake.stash
# MinGW cross-compilation deps (local build environment)
/deps-mingw/
# Snapshot files
*.utx
# Merge artifacts
*.orig
# Dev patches
*.patch
+3
View File
@@ -4,3 +4,6 @@
[submodule "src/secp256k1"]
path = src/secp256k1
url = https://github.com/bitcoin-core/secp256k1
[submodule "src/i2p/i2pd-src"]
path = src/i2p/i2pd-src
url = https://github.com/PurpleI2P/i2pd.git
+91
View File
@@ -0,0 +1,91 @@
# Boost removal — progress
Goal: drop the Boost dependency in favor of C++17 std. No consensus or wire
behavior changes.
## Done
**Triangles' own code (daemon + GUI) is now completely Boost-free.** All nine
translation units that used Boost have been migrated. The only remaining Boost
usage in the tree is (1) the Boost.Test unit-test framework under `src/test/`,
and (2) Boost as a *transitive link dependency of the bundled embedded i2pd
router* (`libi2pd.a`) — not of any Triangles source. See "Remaining" below.
| File | Boost removed | Replacement |
|------|---------------|-------------|
| `txdb-leveldb.cpp` | `boost/version.hpp` (unused include) | deleted |
| `txdb-rocksdb.cpp` | `boost/version.hpp` (unused include) | deleted |
| `walletdb.cpp` | `boost/version.hpp` + `BOOST_VERSION` guard | unconditional `std::filesystem` branch |
| `util.cpp` | `boost::program_options` config-file parser + `to_internal` workaround | small C++17 INI parser in `ReadConfigFile` |
| `init.cpp` | `boost::interprocess::file_lock` + `using namespace boost` | portable `LockDataDirectory()` (`flock` POSIX / `LockFileEx` Win32) |
| `rpcdump.cpp` | `boost::posix_time` + `boost::gregorian` | `std::get_time` + `timegm`/`_mkgmtime` |
`wallet.cpp` and `triangles-cli.cpp` only ever *mentioned* Boost in comments —
no code change needed.
### Behavior notes for review
- **Config parser**: `name = value`; a line whose first non-whitespace char is
`#` is a comment; blank lines ignored; inline `#` is NOT a comment (so
`rpcpassword` may contain `#`). First value wins for single-valued settings;
`-name` keying and `nofoo=` negative-setting interpretation preserved.
- **File lock**: exclusive, non-blocking; the fd/handle is held for process
lifetime and released by the OS on exit (matches the old file_lock lifetime).
- **Dump time parser**: same five accepted formats, parsed as UTC.
### CMake note
`program_options` is no longer used by any source file and can be dropped from
the `find_package(Boost ... COMPONENTS ...)` list once the remaining two files
are migrated. It is left in place for now because removing it before the Asio
migration provides no benefit and the component is harmless if installed.
### RPC server (done — `trianglesrpc.cpp`)
The JSON-RPC/HTTP server previously used `boost::asio` (async sockets +
`boost::asio::ssl`), `boost::bind`, `boost::iostreams`,
`boost::shared_ptr`/`weak_ptr`, and `boost::system::error_code`. It was
rewritten onto **raw BSD sockets** behind a small `std::iostream`
(`src/rpc_httpsocket.h`), preserving the thread-per-connection model so the
HTTP parser, JSON-RPC dispatch, REST handler, and the blocking SSE handler are
all unchanged.
- New `src/rpc_httpsocket.h`: `CSocketIOStream` (a `std::iostream` over a
`SOCKET`), `ConnectRPCSocket()`, `BindRPCSockets()` (separate IPv4/IPv6
listeners, loopback unless `-rpcallowip`), `SockaddrToString()`.
- `ThreadRPCServer2` now binds sockets and runs a `select()`-based accept loop
that spawns `ThreadRPCServer3` per connection.
- `ClientAllowed` takes a numeric IP string.
- `CallRPC` connects via a raw socket.
- **`-rpcssl` is removed.** RPC TLS was a rarely used Asio::ssl feature; for
remote access, front the port with stunnel/nginx or reach it over SSH/Tor
(the same decision Bitcoin Core made). A warning is logged if `-rpcssl` is set.
### Qt URI handler (done — `qt/qtipcserver.cpp`)
The `triangles:` single-instance URI handoff used
`boost::interprocess::message_queue` + `boost::posix_time`. Rewritten onto
`QLocalServer` / `QLocalSocket` (QtNetwork), keeping the existing polling-thread
model via the blocking `waitForNewConnection` / `waitForReadyRead` /
`waitForConnected` methods (no Qt event loop required). `Qt5::Network` added to
the Qt find_package and the `triangles-qt` link.
### CMake
- `Boost::program_options`, `Boost::thread`, `Boost::chrono` removed from the
`triangles_common` link — Triangles' own objects reference no Boost symbols.
## Remaining
Two things still pull Boost into the build; neither is Triangles source:
1. **Embedded i2pd router.** When built with the embedded I2P router, the
bundled `libi2pd.a` / `libi2pdclient.a` link Boost
(`program_options`, `thread`, `chrono`, `filesystem`, `system`). The
i2pd-specific link block (and the top-level `find_package(Boost ...)`) are
therefore left intact. Fully dropping Boost from the build requires either a
Boost-free i2pd build or disabling the embedded router. This is an upstream
i2pd concern, not Triangles code.
2. **Unit tests.** `src/test/*` use the Boost.Test framework
(`Boost::unit_test_framework`). Optional follow-up: port to a header-only
framework (e.g. Catch2/doctest) to remove the last first-party Boost use.
When both are addressed, `find_package(Boost ...)` can be removed entirely.
+95 -2
View File
@@ -54,11 +54,30 @@ option(USE_IPV6 "Enable IPv6 support" ON)
option(USE_QRCODE "Enable QR code generation via libqrencode" OFF)
option(USE_DBUS "Enable D-Bus notifications (Linux only)" ON)
option(USE_ZMQ "Enable ZMQ publisher support" OFF)
option(USE_TOR_EMBEDDED "Enable embedded Tor library linking" OFF)
# Triangles is Tor-native. Tor is REQUIRED — disabling it at build time is
# not a supported configuration. The 2026-06-23 DNS2 clearnet-fork incident
# (5+ days on a parallel chain because someone flipped -notor=1 for
# troubleshooting and never reverted it) motivated this. We keep the option
# for legacy recovery workflows, but default it ON and abort the build if
# anyone explicitly disables it.
option(USE_TOR_EMBEDDED "Enable embedded Tor library linking" ON)
if(DEFINED USE_TOR_EMBEDDED AND NOT USE_TOR_EMBEDDED)
message(FATAL_ERROR
"USE_TOR_EMBEDDED=OFF is not supported. Triangles is Tor-native. "
"If you need clearnet mode for bootstrap recovery, build with "
"USE_TOR_EMBEDDED=ON and pass -notor=1 -recovery-mode=1 at runtime "
"instead.")
endif()
option(USE_O3 "Use -O3 optimization instead of -O2" OFF)
option(ENABLE_PIE "Build position-independent executables" OFF)
option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF)
# Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor.
# When enabled, Triangles supports dual-network anonymity: Tor (.onion) +
# I2P (.b32.i2p). Disabled by default until seed nodes are deployed.
option(USE_I2P_EMBEDDED "Enable embedded I2P (i2pd) library linking" OFF)
set(I2P_SOURCE_ROOT "" CACHE PATH "Path to i2pd source tree (for USE_I2P_EMBEDDED)")
# Cache variables for custom dependency paths
set(BDB_INCLUDE_PATH "" CACHE PATH "Path to Berkeley DB headers")
set(BDB_LIB_PATH "" CACHE PATH "Path to Berkeley DB libraries")
@@ -75,6 +94,7 @@ include(AddCompilerFlags)
find_package(OpenSSL REQUIRED)
find_package(Boost 1.71 REQUIRED COMPONENTS
program_options thread chrono
OPTIONAL_COMPONENTS filesystem system
)
if(BUILD_TESTS)
find_package(Boost REQUIRED COMPONENTS unit_test_framework)
@@ -134,6 +154,78 @@ if(NOT TARGET RocksDB::rocksdb AND NOT TARGET PkgConfig::RocksDB)
message(STATUS "Found RocksDB (manual probe): ${ROCKSDB_LIBRARY}")
endif()
# Modernization: SQLite3 for the new wallet DB backend.
find_package(SQLite3 REQUIRED)
# Triangles uses RocksDB features that only exist in 7.4+ (XXH3 per-block
# checksum, type 4). Building against an older RocksDB produces a binary
# whose smsgDB Open() fails on any SST file written by RocksDB 7.4+ —
# instead of just bailing, src/smessage.cpp::SecMsgDB::Open now
# quarantines the offending file and recovers. We still fail loudly at
# configure time so this drift doesn't sneak back in unnoticed.
# rocksdb/version.h ships with every RocksDB release (3.x onward) and
# defines ROCKSDB_MAJOR / ROCKSDB_MINOR / ROCKSDB_PATCH. If neither
# find_package nor pkg-config exposed RocksDB_VERSION (e.g. Ubuntu 22.04's
# librocksdb-dev, which ships no CMake config and no .pc file), we can
# still recover the version directly from the header. This closes the
# "manual probe silently allows old RocksDB" gap that let v5.9.24 ship
# linked to librocksdb 6.11.
function(_tri_detect_rocksdb_version_from_header)
if(RocksDB_VERSION)
return()
endif()
foreach(_dir ${ARGN})
if(NOT IS_DIRECTORY "${_dir}")
continue()
endif()
set(_vh "${_dir}/rocksdb/version.h")
if(EXISTS "${_vh}")
file(STRINGS "${_vh}" _maj REGEX "^#define ROCKSDB_MAJOR ")
file(STRINGS "${_vh}" _min REGEX "^#define ROCKSDB_MINOR ")
file(STRINGS "${_vh}" _pat REGEX "^#define ROCKSDB_PATCH ")
if(_maj AND _min AND _pat)
string(REGEX MATCH "[0-9]+" _maj "${_maj}")
string(REGEX MATCH "[0-9]+" _min "${_min}")
string(REGEX MATCH "[0-9]+" _pat "${_pat}")
set(RocksDB_VERSION "${_maj}.${_min}.${_pat}")
set(RocksDB_VERSION "${_maj}.${_min}.${_pat}" PARENT_SCOPE)
message(STATUS "Detected RocksDB version from version.h: ${RocksDB_VERSION}")
return()
endif()
endif()
endforeach()
endfunction()
if(NOT RocksDB_VERSION AND TARGET RocksDB::rocksdb)
get_target_property(_rocksdb_inc RocksDB::rocksdb INTERFACE_INCLUDE_DIRECTORIES)
if(_rocksdb_inc)
_tri_detect_rocksdb_version_from_header(${_rocksdb_inc})
endif()
endif()
if(NOT RocksDB_VERSION AND ROCKSDB_INCLUDE_DIR)
_tri_detect_rocksdb_version_from_header(${ROCKSDB_INCLUDE_DIR})
endif()
if(RocksDB_VERSION AND RocksDB_VERSION VERSION_LESS "7.4.0")
message(FATAL_ERROR
"Triangles requires RocksDB >= 7.4.0 (got ${RocksDB_VERSION}). "
"Older versions cannot read smsgDB files written by RocksDB 7.4+ "
"(XXH3 per-block checksum). "
"On Debian/Ubuntu: install librocksdb-dev >= 7.4 from a backports "
"repo or build RocksDB from source into /usr/local.")
elseif(NOT RocksDB_VERSION)
# No version detectable: headers missing entirely, or ROCKSDB_INCLUDE_DIR
# not pointing at one with rocksdb/version.h. Runtime fallback in
# SecMsgDB::Open covers the gap; print WARNING so build logs flag it.
message(WARNING
"Could not determine RocksDB version (no CMake config, no "
"pkg-config metadata, and no rocksdb/version.h found). "
"Triangles prefers RocksDB >= 7.4.0; older versions are recovered "
"at runtime via SecMsgDB::Open's quarantine fallback.")
endif()
# libsecp256k1 — vendored as a git submodule under src/secp256k1. Provides
# ECDSA signing/verification, pubkey recovery (via the recovery module), and
# ECDH for secure messaging. Configure the submodule's build for our needs:
@@ -159,7 +251,7 @@ set(SECP256K1_ENABLE_MODULE_ELLSWIFT OFF CACHE INTERNAL "")
add_subdirectory(src/secp256k1 EXCLUDE_FROM_ALL)
if(BUILD_QT)
find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets)
find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets Network)
find_package(Qt5 COMPONENTS LinguistTools QUIET)
if(USE_DBUS AND UNIX AND NOT APPLE)
find_package(Qt5 COMPONENTS DBus QUIET)
@@ -194,6 +286,7 @@ message(STATUS " QR code: ${USE_QRCODE}")
message(STATUS " D-Bus: ${USE_DBUS}")
message(STATUS " ZMQ: ${USE_ZMQ}")
message(STATUS " Embedded Tor: ${USE_TOR_EMBEDDED}")
message(STATUS " Embedded I2P: ${USE_I2P_EMBEDDED}")
message(STATUS " Static linking: ${ENABLE_STATIC}")
message(STATUS " ccache: ${CCACHE_PROGRAM}")
message(STATUS " Unity build: ${ENABLE_UNITY_BUILD}")
+1 -1
View File
@@ -2,7 +2,7 @@ FROM ubuntu:22.04
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="5.7.6"
LABEL version="6.1.0"
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
+237
View File
@@ -0,0 +1,237 @@
# I2P Embedded Architecture (Level 3)
**Date:** 2026-06-27
**Status:** ✅ IMPLEMENTED & WORKING
---
## What This Is
Triangles now runs **two embedded anonymity networks simultaneously**:
1. **Tor** — Every node is a .onion hidden service (existing, unchanged)
2. **I2P** — Every node is a .b32.i2p destination (new)
Both routers run **in-process** as static libraries. No external dependencies, no separate daemons to install.
### What I2P Adds Over Tor-Only
| Property | Tor | I2P |
|----------|-----|-----|
| Routing | Onion (3-hop circuits) | Garlic (variable-hop tunnels) |
| Directory | Centralized authorities | Distributed floodfills |
| Service discovery | Hidden service descriptors | Network database (KadDHT) |
| Designed for | Exit to clearnet | Peer-to-peer services |
| Peer correlation resistance | Moderate | Strong (ephemeral tunnels) |
I2P was designed from the ground up for **peer-to-peer anonymous services** — exactly what a cryptocurrency P2P network needs. Tor's hidden services work, but Tor is optimized for anonymous web browsing (exit traffic). I2P's garlic routing, distributed network database, and short-lived tunnels make it inherently better suited for P2P mesh communication.
---
## Architecture
### Dual-Network Routing
```
┌─────────────────────────────────┐
│ trianglesd (process) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ libtor │ │ libi2pd │ │
│ │ (Tor) │ │ (I2P) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
.onion peers ─────┼───────┘ │ │
│ SOCKS 19099 │ │
│ │ │
.b32.i2p peers ───┼──────────────────────┘ │
│ SOCKS 19100 │
└─────────────────────────────────┘
```
### Traffic Flow
| Destination | Route | Proxy |
|-------------|-------|-------|
| `*.onion` | Tor SOCKS5 → Tor circuit → hidden service | 127.0.0.1:19099 |
| `*.b32.i2p` | I2P SOCKS5 → I2P tunnel → destination | 127.0.0.1:19100 |
| Clearnet (IPv4/IPv6) | **BLOCKED** | — |
The routing decision happens in `ConnectSocketByName()` (netbase.cpp):
- `.b32.i2p` suffix → I2P SOCKS proxy (NET_I2P)
- Everything else → Tor name proxy (SetNameProxy)
---
## Implementation
### Files Added
```
src/i2p/
├── i2pd-src/ # PurpleI2P/i2pd git submodule
├── i2p_embedded.h # CI2PEmbedded class declaration
├── i2p_embedded.cpp # Embedded router start/stop logic
├── i2pseed.h # Hardcoded .b32.i2p seed nodes
└── build-libi2pd.sh # Static library build script
```
### Files Modified
| File | Change |
|------|--------|
| `CMakeLists.txt` | `USE_I2P_EMBEDDED` option + config summary |
| `src/CMakeLists.txt` | I2P source, includes, library linking |
| `src/init.cpp` | I2P startup (after Tor), shutdown, CLI flags |
| `src/net.cpp` | Allow `.b32.i2p` in `ConnectNode()` and seed parser |
| `src/netbase.cpp` | I2P SOCKS routing, fixed `.b32.i2p` address parsing |
### CI2PEmbedded Class
Singleton pattern (mirrors `CTorEmbedded`):
```cpp
class CI2PEmbedded {
bool Start(int socksPort, int samPort, int serverPort);
void Stop();
bool IsRunning() const;
std::string GetSocksProxy() const; // "127.0.0.1:19100"
std::string GetI2PAddress() const; // .b32.i2p destination
};
```
### Startup Sequence (init.cpp)
```
1. StartEmbeddedTor() → Tor SOCKS on 19099
2. TOR-NATIVE MODE → all traffic forced through Tor
3. StartEmbeddedI2P() → i2pd SOCKS on 19100
4. I2P-NATIVE MODE → .b32.i2p routed through i2pd
5. Dual-network anonymity → Tor + I2P co-equal
```
If I2P fails to start, the daemon continues in Tor-only mode (non-fatal).
### How i2pd Integrates
i2pd provides a C++ API (`libi2pd/api.h`) for in-process embedding:
```cpp
i2p::api::InitI2P(argc, argv, "triangles-i2pd");
i2p::api::StartI2P(logStream);
i2p::client::context.Start(); // SAM, SOCKS, tunnels
```
The auto-generated `i2pd.conf` enables:
- SOCKS proxy on 19100 (for outbound .b32.i2p)
- SAM bridge on 7656 (for future SAM v3 protocol)
- Server tunnel in `tunnels.conf` (I2P hidden service)
The `tunnels.conf` is written before `Start()`:
```ini
[triangles-p2p]
type = server
host = 127.0.0.1
port = <P2P_PORT>
keys = triangles-p2p-keys.dat
inbound.length = 3
outbound.length = 3
```
This creates a persistent `.b32.i2p` destination that survives restarts.
---
## Build Instructions
### Prerequisites
Same as existing Tor build + Boost (already required).
### Build with I2P
```bash
# 1. Initialize the i2pd submodule
git submodule update --init --recursive src/i2p/i2pd-src
# 2. Build i2pd static libraries
cd src/i2p && bash build-libi2pd.sh
# 3. Configure and build Triangles
mkdir build && cd build
cmake -G Ninja -DUSE_I2P_EMBEDDED=ON ..
ninja trianglesd
```
### Build without I2P (Tor-only, existing behavior)
```bash
cmake -G Ninja .. # USE_I2P_EMBEDDED defaults to OFF
ninja trianglesd
```
---
## CLI Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-i2p` | `1` | Enable embedded I2P router |
| `-i2psocks=<port>` | `19100` | I2P SOCKS proxy port |
| `-i2psam=<port>` | `7656` | I2P SAM bridge port |
| `-i2phsport=<port>` | P2P port | I2P server tunnel forward port |
---
## Testing Verification
### Expected Startup Output
```
Embedded I2P: starting i2pd router...
Embedded I2P: server tunnel configured on port 24112
...
Clients: New private keys file .../triangles-p2p-keys.dat for <b32>.b32.i2p created
Clients: 1 I2P server tunnels created
Embedded I2P: SOCKS proxy at 127.0.0.1:19100, SAM at 127.0.0.1:7656
...
I2P-NATIVE MODE: I2P router running
SOCKS proxy at 127.0.0.1:19100 for .b32.i2p connections
Dual-network anonymity: Tor (.onion) + I2P (.b32.i2p)
```
---
## Seed Node Deployment
To deploy an I2P seed node:
1. Build with `-DUSE_I2P_EMBEDDED=ON`
2. Start the daemon — it auto-generates a `.b32.i2p` destination
3. Read the address from the log: `grep "b32.i2p" debug.log`
4. Add the address to `src/i2p/i2pseed.h`
5. Add the address to `seeds.cryptographic-triangles.org/i2p-seeds.txt`
The destination keys persist in `<datadir>/i2p_data/triangles-p2p-keys.dat`.
---
## Comparison to Other Projects
| Project | Tor | I2P | Embedded | Dual-Network |
|---------|-----|-----|----------|-------------|
| **Triangles** | ✅ Embedded | ✅ Embedded | Both in-process | ✅ |
| Bitcoin Core | Optional | Optional (SAM) | No | No |
| Monero | Optional | No | No | No |
| Kovri (Monero I2P) | N/A | Planned | Planned | No |
Triangles is the only cryptocurrency with **both** Tor and I2P embedded as in-process routers.
---
## Future Work
- **I2P seed nodes:** Deploy stable .b32.i2p seeds (parallel to onion seeds)
- **SAM v3 direct:** Use SAM bridge for native I2P streaming (bypass SOCKS overhead)
- **I2P address in RPC:** Expose `.b32.i2p` address via `getnetworkinfo`
- **Cross-network bridging:** Allow Tor nodes to discover I2P peers and vice versa
+272 -250
View File
@@ -1,250 +1,272 @@
# Cryptographic Triangles (TRI)
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
## Key Features
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
## Specifications
| Property | Value |
|----------|-------|
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
| Block Time | ~120 seconds |
| Max Supply | 2,222,222 TRI |
| PoS Reward | 33% annual, coin-age based |
| P2P Port | 24112 |
| RPC Port | 19112 |
| Protocol | 70205 |
## Network Status
The Triangles network operates exclusively over Tor for privacy:
**Tor v3 Seeds:**
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
**HTTP Seed List:**
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
## Building from Source
Triangles uses CMake. All platforms follow the same build pattern.
### Dependencies
| Dependency | Minimum Version |
|------------|----------------|
| CMake | 3.16+ |
| C++ compiler | C++17 support |
| OpenSSL | 3.x |
| Boost | 1.90+ |
| Berkeley DB | 5.3 (with C++ bindings) |
| libevent | 2.x |
| LevelDB | bundled |
### Linux (Ubuntu 24.04 / Debian 12+)
Install dependencies:
```bash
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
zlib1g-dev libminiupnpc-dev
```
For the Qt wallet, also install:
```bash
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
```
Build:
```bash
cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
```
### Linux (AlmaLinux 9 / RHEL 9)
Install dependencies:
```bash
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
libevent-devel zlib-devel miniupnpc-devel
```
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
Then build as above.
### Windows (MSYS2 MinGW64)
Open an MSYS2 MinGW64 shell and install:
```bash
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
mingw-w64-x86_64-libevent
```
Build:
```bash
cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
```
### Build Options
| Option | Default | Description |
|--------|---------|-------------|
| `BUILD_QT` | ON | Build the Qt GUI wallet |
| `BUILD_DAEMON` | ON | Build the headless daemon |
| `BUILD_TESTS` | OFF | Build unit tests |
## Running
### First Run
```bash
mkdir -p ~/.triangles
cat > ~/.triangles/triangles.conf << 'EOF'
port=24112
rpcport=19112
rpcuser=trianglesrpc
rpcpassword=<generate-a-strong-password>
rpcallowip=127.0.0.1
staking=1
txindex=1
listen=1
server=1
daemon=1
proxy=127.0.0.1:9050
EOF
trianglesd
```
The node will connect to seed nodes over Tor and sync the blockchain automatically.
### Existing Wallet Holders
If you have a `wallet.dat` from the original Triangles network:
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
3. No migration or special action is needed - all keys and balances are preserved
### Staking
To stake, your wallet must be:
- Running with `staking=1` in the config
- Connected to at least one peer
- Containing coins with sufficient coin-age (mature inputs)
Check staking status:
```bash
trianglesd getstakinginfo
```
### Encrypted Messaging
Send and receive encrypted messages between wallet addresses:
```bash
# Enable messaging
trianglesd smsgenable
# Send a message
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
# Check inbox
trianglesd smsginbox all
# Send anonymous message
trianglesd smsgsendanon <recipient-address> "Anonymous message"
```
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
### Tor Support
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
```
# triangles.conf
proxy=127.0.0.1:9050
```
To run your own hidden service, add to `/etc/tor/torrc`:
```
HiddenServiceDir /var/lib/tor/triangles/
HiddenServiceVersion 3
HiddenServicePort 24112 127.0.0.1:24112
```
Then set `externalip=<your-onion-address>` in `triangles.conf`.
## RPC Commands
### General
- `getinfo` - Node status, balance, block height, connections
- `getpeerinfo` - Connected peer details
- `getstakinginfo` - Staking status and weight
### Wallet
- `getbalance` - Current balance
- `listunspent` - Unspent transaction outputs
- `sendtoaddress <addr> <amount>` - Send TRI
- `getnewaddress` - Generate new receiving address
### Messaging
- `smsgenable` / `smsgdisable` - Toggle secure messaging
- `smsgsend <from> <to> <message>` - Send encrypted message
- `smsgsendanon <to> <message>` - Send anonymous message
- `smsginbox [all|unread|clear]` - View received messages
- `smsgoutbox [all|clear]` - View sent messages
- `smsglocalkeys` - List messaging-enabled addresses
- `smsgscanchain` - Scan blockchain for public keys
## Chain History
- **July 16, 2014** - Genesis block
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
- **Block 9001+** - Proof-of-Stake only
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
- **December 8, 2022** - Chain frozen (all nodes offline)
- **March 11, 2026** - Chain revived, staking resumed
## Project Structure
```
src/
main.cpp - Core blockchain logic, block/tx validation, message routing
miner.cpp - Staking miner thread
net.cpp - P2P networking
init.cpp - Daemon initialization
wallet.cpp - Wallet management
smessage.cpp/h - Encrypted messaging system
kernel.cpp - PoS kernel (stake validation)
checkpoints.cpp - Hardcoded checkpoints
net_bootstrap.h - DNS/IP seed configuration
onionseed.h - Tor v3 onion seed addresses
tor/
onion_v3.cpp/h - Tor v3 hidden service management
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
```
## License
Distributed under the MIT/X11 software license. See `COPYING` for details.
## Links
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
# Cryptographic Triangles (TRI)
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
## Key Features
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
## Specifications
| Property | Value |
|----------|-------|
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
| Block Time | ~120 seconds |
| Max Supply | 2,222,222 TRI |
| PoS Reward | 33% annual, coin-age based |
| P2P Port | 24112 |
| RPC Port | 19112 |
| Protocol | 70205 |
## Network Status
The Triangles network operates exclusively over Tor for privacy:
**Tor v3 Seeds:**
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
**HTTP Seed List:**
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
## Building from Source
Triangles uses CMake. All platforms follow the same build pattern.
### Dependencies
| Dependency | Minimum Version |
|------------|----------------|
| CMake | 3.16+ |
| C++ compiler | C++17 support |
| OpenSSL | 3.x |
| Boost | 1.90+ |
| SQLite | 3.x (default wallet database backend) |
| Berkeley DB | 5.3 with C++ bindings (legacy wallet backend, used for migration) |
| libevent | 2.x |
| RocksDB | 7.4+ (default chain database backend) |
| LevelDB | bundled (legacy chain DB backend, used for migration) |
### Linux (Ubuntu 24.04 / Debian 12+)
Install dependencies:
```bash
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
zlib1g-dev libminiupnpc-dev
```
For the Qt wallet, also install:
```bash
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
```
Build:
```bash
cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
```
### Linux (AlmaLinux 9 / RHEL 9)
Install dependencies:
```bash
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
libevent-devel zlib-devel miniupnpc-devel
```
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
Then build as above.
### Windows (MSYS2 MinGW64)
Open an MSYS2 MinGW64 shell and install:
```bash
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
mingw-w64-x86_64-libevent
```
Build:
```bash
cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
```
### Build Options
| Option | Default | Description |
|--------|---------|-------------|
| `BUILD_QT` | ON | Build the Qt GUI wallet |
| `BUILD_DAEMON` | ON | Build the headless daemon |
| `BUILD_TESTS` | OFF | Build unit tests |
## Running
### First Run
```bash
mkdir -p ~/.triangles
cat > ~/.triangles/triangles.conf << 'EOF'
port=24112
rpcport=19112
rpcuser=trianglesrpc
rpcpassword=<generate-a-strong-password>
rpcallowip=127.0.0.1
staking=1
txindex=1
listen=1
server=1
daemon=1
proxy=127.0.0.1:9050
EOF
trianglesd
```
The node will connect to seed nodes over Tor and sync the blockchain automatically.
### Chain Database (RocksDB)
The chain database (block index, transaction index, UTXO set, address index) uses **RocksDB by default**. RocksDB gives faster sync and lookups than the legacy LevelDB backend through parallel compaction, bloom filters, and a larger write buffer and block cache (tunable with `-dbcache=<MB>`).
If you are upgrading a node that already has a LevelDB chain database (`txleveldb/` in your data directory), it is migrated automatically on first launch: the chain state is copied into a new `rocksdb/` directory and verified (record count, UTXO count and value, best-chain hash, and DB format must all match) before use. The original `txleveldb/` directory is left untouched as a fallback and is never modified.
To select a backend explicitly:
```bash
trianglesd -chaindb=rocksdb # default
trianglesd -chaindb=leveldb # legacy backend (retained for fallback/migration)
```
Migration can also be triggered or forced manually:
```bash
trianglesd -migratechaindb # migrate txleveldb -> rocksdb if not already done
trianglesd -migratechaindbforce # re-migrate, replacing any existing rocksdb/
```
### Existing Wallet Holders
If you have a `wallet.dat` from the original Triangles network:
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
3. No migration or special action is needed - all keys and balances are preserved
### Staking
To stake, your wallet must be:
- Running with `staking=1` in the config
- Connected to at least one peer
- Containing coins with sufficient coin-age (mature inputs)
Check staking status:
```bash
trianglesd getstakinginfo
```
### Encrypted Messaging
Send and receive encrypted messages between wallet addresses:
```bash
# Enable messaging
trianglesd smsgenable
# Send a message
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
# Check inbox
trianglesd smsginbox all
# Send anonymous message
trianglesd smsgsendanon <recipient-address> "Anonymous message"
```
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
### Tor Support
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
```
# triangles.conf
proxy=127.0.0.1:9050
```
To run your own hidden service, add to `/etc/tor/torrc`:
```
HiddenServiceDir /var/lib/tor/triangles/
HiddenServiceVersion 3
HiddenServicePort 24112 127.0.0.1:24112
```
Then set `externalip=<your-onion-address>` in `triangles.conf`.
## RPC Commands
### General
- `getinfo` - Node status, balance, block height, connections
- `getpeerinfo` - Connected peer details
- `getstakinginfo` - Staking status and weight
### Wallet
- `getbalance` - Current balance
- `listunspent` - Unspent transaction outputs
- `sendtoaddress <addr> <amount>` - Send TRI
- `getnewaddress` - Generate new receiving address
### Messaging
- `smsgenable` / `smsgdisable` - Toggle secure messaging
- `smsgsend <from> <to> <message>` - Send encrypted message
- `smsgsendanon <to> <message>` - Send anonymous message
- `smsginbox [all|unread|clear]` - View received messages
- `smsgoutbox [all|clear]` - View sent messages
- `smsglocalkeys` - List messaging-enabled addresses
- `smsgscanchain` - Scan blockchain for public keys
## Chain History
- **July 16, 2014** - Genesis block
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
- **Block 9001+** - Proof-of-Stake only
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
- **December 8, 2022** - Chain frozen (all nodes offline)
- **March 11, 2026** - Chain revived, staking resumed
## Project Structure
```
src/
main.cpp - Core blockchain logic, block/tx validation, message routing
miner.cpp - Staking miner thread
net.cpp - P2P networking
init.cpp - Daemon initialization
wallet.cpp - Wallet management
smessage.cpp/h - Encrypted messaging system
kernel.cpp - PoS kernel (stake validation)
checkpoints.cpp - Hardcoded checkpoints
net_bootstrap.h - DNS/IP seed configuration
onionseed.h - Tor v3 onion seed addresses
tor/
onion_v3.cpp/h - Tor v3 hidden service management
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
```
## License
Distributed under the MIT/X11 software license. See `COPYING` for details.
## Links
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
+132
View File
@@ -0,0 +1,132 @@
# RocksDB as the default chain database backend
This change finishes the RocksDB chain-database backend, makes it the default,
and provides a transparent migration path off LevelDB. **No consensus rules
change** — only how the block index / tx index / UTXO set / address index are
stored on disk. On-disk key bytes remain identical across both backends, which
is what the migration and the dual-backend equivalence tests rely on.
## What changed
### 1. Fixed the column-family iteration bug (the real "unfinished" blocker)
The RocksDB backend routed keys into per-prefix **column families**
(`blockindex`, `txindex`, `utxo`, `addrindex`) on write, but the read path —
both `CRocksTxDB::NewIterator()` and `CRocksTxDB::LoadBlockIndex()` — only ever
iterated the **default** column family. With column families enabled:
- `LoadBlockIndex()` loaded **zero** blocks (block-index records were in a
non-default CF the loader never scanned),
- UTXO snapshot dumps and address-index range scans saw nothing, and
- the migration verifier `CollectStats()` reported a record-count mismatch.
This is why `-chaindb=rocksdb` "compiled clean but was never runtime-valid."
**Fix:** column-family partitioning is disabled. `GetCF()` now always returns
the default CF, so writes, point reads, `Exists`, `Erase`, and full-keyspace
iteration are mutually consistent — and byte-identical to the single-keyspace
LevelDB backend. New databases are created single-CF; pre-existing experimental
multi-CF databases are still opened (for compatibility) but should be
re-migrated or reindexed. RocksDB still delivers its performance win from
parallel compaction, bloom filters, large write buffer, and block cache — the
CF split was a premature optimization, not the source of the speedup.
Re-introducing column families is a tracked follow-up that first requires
CF-aware iterators (a multiplexed merge across CFs) in `NewIterator()` /
`LoadBlockIndex()`.
### 2. Automatic LevelDB -> RocksDB migration on startup
`init.cpp` now runs the migration automatically when RocksDB is the active
backend and the only chain DB present is a legacy `txleveldb/` (no `rocksdb/`
yet). `MaybeMigrateLevelDbToRocksDb()` is a no-op when there is nothing to
migrate, so it is safe on every launch. The LevelDB source is never modified;
it remains a fallback.
### 3. RocksDB is now the default backend
`-chaindb` defaults to `rocksdb` (was `leveldb`). LevelDB stays selectable with
`-chaindb=leveldb` and is retained as migration source + fallback. Full removal
of LevelDB is deferred to a later phase, after live-chain validation.
### 4. Fixed `NeedsBootstrap()` to recognize the RocksDB directory
`Bootstrap::NeedsBootstrap()` checked for `txleveldb/` but not `rocksdb/`. With
RocksDB as default, a fully-synced rocksdb-only node would have been treated as
"fresh" and could have triggered a bootstrap download over a healthy chain on
every restart. It now treats a `rocksdb/` directory as an existing chain DB.
## Files changed
- `src/txdb-rocksdb.cpp` — disable CF routing; single-CF open; remove dead CF tables
- `src/txdb-rocksdb.h` — update CF member docs
- `src/txdb-factory.cpp` — default backend `leveldb` -> `rocksdb`
- `src/txdb.h` — update factory doc comment
- `src/init.cpp` — auto-migrate on startup when RocksDB active + legacy LevelDB present
- `src/bootstrap.cpp``NeedsBootstrap()` recognizes `rocksdb/`
- `src/test/chaindb_runtime_tests.cpp` — update default-backend expectations
- `README.md` — document RocksDB default + migration
## Build
```bash
cmake -B build -G Ninja -DBUILD_QT=ON -DBUILD_TESTS=ON
cmake --build build
```
RocksDB is required (`librocksdb-dev` >= 7.4 on Debian/Ubuntu,
`mingw-w64-x86_64-rocksdb` on MSYS2, `rocksdb` on Homebrew).
## Tests
```bash
# RocksDB wrapper runtime smoke tests (the class the daemon uses at runtime)
./build/bin/test_chaindb_runtime
# LevelDB/RocksDB byte-for-byte migration equivalence
./build/bin/test_chaindb_equivalence
# Full unit suite
./build/bin/test_triangles
```
Expected after this change:
- `get_chain_data_dir_default_is_rocksdb` passes (default resolves to rocksdb).
- `iterator_walks_every_key_in_sorted_order` passes (the `"banana"` key, which
previously routed to a non-default CF the iterator never read, now lives in
the default CF and is iterated).
- Migration verification (`CollectStats` / `StatsMatch`) passes end-to-end.
## Live-chain validation checklist (V6 task T010)
This is the step that cannot be done without real chain data and must be run on
a node before release:
1. **Migrate a real chain.** On a node with an existing `txleveldb/`, launch the
new binary (default backend). Confirm the log shows
`ChainDB: RocksDB backend active with a legacy LevelDB present; migrating
automatically.` followed by `ChainDB migration: verified N records ... best=<hash>`.
2. **Verify block index loads.** Confirm `LoadBlockIndex()` reports the correct
`height=` and `hashBestChain=` (matching the prior LevelDB tip), not 0.
3. **Compare RPC output.** `getinfo`, `getblockcount`, `getbestblockhash`, and a
spot-check of `gettxout` / address-index queries must match a LevelDB run of
the same datadir (`-chaindb=leveldb`).
4. **Restart twice.** Confirm no spurious bootstrap download fires and the tip is
stable across restarts.
5. **Sync new blocks.** Let the node accept and stake new blocks; confirm UTXO
set and money supply stay consistent.
6. **Benchmark.** Use `contrib/bench/bench-chaindb.sh --backends=rocksdb` vs
`leveldb` to confirm the speedup on this hardware.
## Rollback
Set `-chaindb=leveldb` in `triangles.conf` (or on the command line). The
original `txleveldb/` is untouched by migration, so reverting is immediate.
## Remaining follow-ups
- CF-aware iteration, then re-enable column-family partitioning for independent
compaction/caching.
- Retire LevelDB entirely (remove `txdb-leveldb.*`, drop the `-chaindb=leveldb`
option and the bundled LevelDB dependency) once RocksDB is validated in
production for at least one release cycle.
+98
View File
@@ -0,0 +1,98 @@
# Wallet storage: Berkeley DB → SQLite
Goal: retire Berkeley DB as the wallet store and make **SQLite the default**
wallet backend, with a transparent, non-destructive migration of existing
`wallet.dat` files. This removes the single ugliest build dependency (BDB 5.3
with C++ bindings, hand-built on RHEL/MSYS2) and gives the wallet a modern,
maintainable, single-file store — the kind exchanges expect.
No consensus or wire behavior changes. The on-disk *record encoding* is
unchanged: keys and values are the exact `SER_DISK / CLIENT_VERSION` bytes
`CWalletDB` already produces, just stored as `(key BLOB, value BLOB)` rows in
SQLite instead of Berkeley B-tree entries. That byte-for-byte identity is what
makes migration a verbatim copy.
## Delivered in this pass
New, self-contained modules (do not disturb the working Berkeley path):
| File | Purpose |
|------|---------|
| `src/walletdb-base.h` | Backend-agnostic seam: `WalletDatabase`, `WalletBatch` (raw byte Read/Write/Erase/Has + cursor + txn), `WalletCursor`; `ResolveWalletDbKind()` / `MakeWalletDatabase()` declarations. |
| `src/walletdb-sqlite.h/.cpp` | `SQLiteDatabase` / `SQLiteBatch` — single `main(key BLOB PRIMARY KEY, value BLOB)` table, `synchronous=FULL`, prepared statements, transactions, cursor, online-backup, `integrity_check`. App-id/user-version stamping to reject foreign DBs. |
| `src/walletmigrate.h/.cpp` | `MaybeMigrateBerkeleyWalletToSQLite()` — detects a Berkeley `wallet.dat`, copies every record verbatim into a temp SQLite file, verifies the row count, backs up the original to `wallet.dat.bdb.bak`, then swaps SQLite into place. Idempotent and non-destructive. |
| `src/walletdb-factory.cpp` | `ResolveWalletDbKind()` (default **sqlite**, `-walletdb=bdb` fallback) and `MakeWalletDatabase()` (SQLite implemented). |
| `src/walletdb-batch.h` | `CWalletBatchTyped` — typed Read/Write/Erase/Exists + cursor over `WalletBatch`, byte-identical to the old `CDB` templates. The drop-in base for `CWalletDB`. |
Build wiring:
- `find_package(SQLite3 REQUIRED)` in the top-level `CMakeLists.txt`.
- `SQLite::SQLite3` linked into `triangles_common`; the new sources added to `CORE_SOURCES`.
## Remaining integration (compile-in-the-loop)
The new modules are complete but `CWalletDB` is not yet routed through the seam
— it still inherits Berkeley `CDB`. This is the mechanical-but-careful step that
needs a compiler in the loop. **It must be done and landed as one unit** (it
touches `walletdb.h`, `walletdb.cpp`, `wallet.cpp`, `db.cpp`, and `init.cpp`):
re-basing ~800 lines of funds-critical code is exactly the kind of change that
should be compiled and run against a real `wallet.dat` rather than committed
blind.
1. **Typed wrappers over the batch — DONE.** `src/walletdb-batch.h`
(`CWalletBatchTyped`) provides `Read/Write/Erase/Exists` + cursor over a
`WalletBatch`, byte-identical to `CDB`'s templates. `CWalletDB` derives from
it instead of `CDB`.
2. **Re-base `CWalletDB`.** Hold a `std::unique_ptr<WalletDatabase>` +
`WalletBatch` obtained from `MakeWalletDatabase("wallet.dat", err)` instead of
deriving from `CDB`. Route `TxnBegin/Commit/Abort` to the batch.
3. **Cursors.** Replace `GetAtCursor` / `GetTxnCursor` / `ReadAtCursor`
(Berkeley `Dbc*`, `DB_NEXT`) in `walletdb.cpp` (`LoadWallet`,
`ReorderTransactions`) with `WalletBatch::GetNewCursor()` + `WalletCursor::Next()`.
4. **Berkeley-specific call sites.**
- `BackupWallet()` / `AutoBackupWallet()``WalletDatabase::Backup()`.
- `CDB::Rewrite()` (used by `CWallet::EncryptWallet`) → `WalletDatabase::Rewrite()`
(VACUUM). Unencrypted-key cleanup already happens via explicit `Erase`.
- `bitdb.Flush()` / env shutdown in `init.cpp``WalletDatabase::Flush()/Close()`
(no-op for SQLite).
5. **Berkeley behind the same seam (optional but recommended).** Add a thin
`BerkeleyDatabase`/`BerkeleyBatch` adapter wrapping the existing `CDBEnv`/`CDB`
so `-walletdb=bdb` routes through `MakeWalletDatabase` too, instead of the
legacy path. Keeps one code path for one release, then delete BDB entirely.
6. **Run the migration on startup.** In `init.cpp`, before the wallet is loaded
and when the backend is SQLite, call
`MaybeMigrateBerkeleyWalletToSQLite(GetDataDir()/strWalletFileName, err)`.
## Gating
```
trianglesd # SQLite (default)
trianglesd -walletdb=bdb # Berkeley fallback (retained for one release)
```
## Validation checklist (must pass before release)
Cannot be verified without a build + a real wallet. Run on a node:
1. **Build** with `-DBUILD_TESTS=ON`; confirm SQLite is found and linked.
2. **Fresh wallet**: start with no wallet → a SQLite `wallet.dat` is created;
`getnewaddress`, `getinfo` work; restart preserves keys/balance.
3. **Migration**: copy a real Berkeley `wallet.dat` into the datadir, start the
node. Confirm: `wallet.dat.bdb.bak` is created, `wallet.dat` is now SQLite
(`sqlite3 wallet.dat "PRAGMA integrity_check;"``ok`), and
`listaddressgroupings` / `getbalance` / `dumpwallet` match a `-walletdb=bdb`
run against the `.bdb.bak` original.
4. **Key parity**: `dumpwallet` before (bdb) and after (sqlite); diff must be
empty (same keys, labels, metadata, HD seed).
5. **Encryption**: `encryptwallet`, restart, `walletpassphrase`, sign/spend.
6. **Backup/restore**: `backupwallet`, restore into a fresh datadir, verify
balance and spend.
7. **Send/receive + staking** over a few blocks; confirm new keys/txns persist
across restart.
8. **Crash safety**: kill -9 mid-write; restart; `integrity_check` ok, no loss.
## Follow-ups
- Add `test_wallet_sqlite` unit tests (round-trip, migration parity, cursor).
- Once SQLite is validated for a release, remove `-walletdb=bdb`, delete
`db.cpp`/`walletdb`'s Berkeley code, and drop the `BerkeleyDB` CMake
dependency — completing the retirement.
+24
View File
@@ -47,6 +47,30 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "i[3-6]86")
add_compile_options(-msse2)
endif()
# ── x86-64 baseline ISA (portability across CPU vendors/models) ──
# CRITICAL: Without this, GCC on Intel CI runners (Skylake-X, Ice Lake,
# Sapphire Rapids) emits AVX-512 / AVX10 instructions (vmovdqu8, vpcompressd,
# vpopcntd, etc.) for std::string / memcpy inlining that CRASH with SIGILL
# on AMD EPYC (Milan, Genoa) and older Intel without AVX-512/AVX10.
# x86-64-v2 = baseline from ~2009 (Nehalem): SSE4.2 + POPCNT + CMPXCHG16B.
# Supported on EVERY x86_64 CPU Triangles runs on in production (DNS2, DNS3,
# Hetzner ARM64 excluded — that's a different build). Do NOT raise to v3
# (AVX2) without re-testing on every supported CPU; v3 is fine for most
# modern hardware but adds risk on edge cases (early Ryzen, Atom).
# Override with -DCMAKE_X86_64_BASELINE=OFF to disable (not recommended).
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" AND NOT WIN32 AND NOT APPLE)
option(CMAKE_X86_64_BASELINE
"Compile with -march=x86-64-v2 (SSE4.2 baseline) for portability across CPU vendors"
ON)
if(CMAKE_X86_64_BASELINE)
add_compile_options(-march=x86-64-v2)
# -mtune=generic tells GCC the binary will run on CPUs other than the
# build host. Combined with -march=x86-64-v2 above, the scheduler
# picks instructions from the v2 subset only — no AVX-512 leaks.
add_compile_options(-mtune=generic)
endif()
endif()
# ── Platform: Windows (MSYS2 MinGW64) ──
if(WIN32)
add_compile_options(-Wa,-mbig-obj)
+70
View File
@@ -0,0 +1,70 @@
# CMake toolchain file for cross-compiling Triangles for Windows x64 using MinGW on Linux
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/mingw64.cmake -B build-mingw -S .
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# MinGW toolchain
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
# Search for programs only in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Search for libraries and headers only in the staging directory
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Staging prefix — all dependencies installed here
set(DEP_PREFIX "${CMAKE_SOURCE_DIR}/deps-mingw")
# Windows libraries
set(CMAKE_LIBRARY_PATH "${DEP_PREFIX}/lib")
# Include directories
set(CMAKE_INCLUDE_PATH "${DEP_PREFIX}/include")
# Windows sysroot (MinGW libraries, headers, and tools)
set(MINGW_SYSROOT /usr/x86_64-w64-mingw32)
# Don't search the host system for programs
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32 ${DEP_PREFIX})
# For find_package(OpenSSL), find_package(Boost), etc.
# Only search deps-mingw and MinGW sysroot — NOT the host system
set(CMAKE_SYSROOT "${MINGW_SYSROOT}")
set(OPENSSL_ROOT_DIR "${DEP_PREFIX}")
set(BOOST_ROOT "${DEP_PREFIX}")
set(CMAKE_PREFIX_PATH "${DEP_PREFIX}")
# Critical: prevent Linux host headers from leaking into MinGW compilation
# The MinGW cross-compiler should ONLY see MinGW and deps headers
set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "")
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "")
# Add MinGW and deps include paths explicitly
include_directories(BEFORE SYSTEM
"${DEP_PREFIX}/include"
"${MINGW_SYSROOT}/include"
"${MINGW_SYSROOT}/include/c++"
"${MINGW_SYSROOT}/include/sec_api"
)
# Set output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
# C++20 for the project
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Build settings
set(BUILD_DAEMON ON)
set(BUILD_QT OFF)
set(BUILD_TESTS OFF)
set(USE_UPNP OFF)
set(USE_QRCODE OFF)
set(USE_ZMQ OFF)
set(USE_DBUS OFF)
set(USE_TOR_EMBEDDED OFF)
+68
View File
@@ -0,0 +1,68 @@
# I2P support (SAM v3)
Triangles runs over I2P in addition to Tor, giving the wallet a second
anonymous network and a `.b32.i2p` address shown directly above the `.onion`
address in the status bar.
I2P is **on by default** and works the same way as the embedded Tor: the wallet
auto-launches a bundled **i2pd** router as a managed child process, enables its
SAM bridge, and connects to it. The user does not have to install or configure
anything — provided the i2pd binary ships with the wallet.
## Shipping the i2pd binary
Like `tor.exe`, the wallet looks for an `i2pd` executable in several places and
launches the first one it finds:
1. Next to the wallet executable (recommended): `i2pd.exe` (Windows) / `i2pd`
(Linux/macOS), or in an `i2pd/` subfolder beside it.
2. In the data directory (or its `i2pd/` subfolder).
3. Common system locations (`/usr/bin/i2pd`, Homebrew, `C:\i2pd\…`, etc.).
Get i2pd from https://i2pd.website/ (or your package manager) and place the
binary next to the wallet in your build/packaging step. That's the only manual
part, and it's a packaging concern, not something the end user does.
If no i2pd binary is found, the wallet logs a notice and continues with **Tor
only** — I2P is strictly additive and never blocks start-up.
## What happens at start-up
1. If a SAM bridge is already listening on `127.0.0.1:7656` (e.g. you run your
own router), the wallet uses it and does **not** launch its own.
2. Otherwise it writes `i2pd.conf` into `<datadir>/i2pd/` (SAM enabled, other
services off), launches i2pd, and waits for the SAM bridge to come up.
3. The SAM client then loads/creates a persistent destination
(`<datadir>/i2p_private_key`), opens a STREAM session, derives the
`.b32.i2p` address (`base32(SHA-256(destination))`), accepts inbound I2P
streams, and dials outbound `.b32.i2p` peers.
4. On wallet exit, the SAM session is closed and the i2pd child process is
terminated (an external router you started yourself is left running).
The first session takes a little longer while i2pd builds tunnels; the address
appears once the bridge is ready.
## Options
```
-i2p Enable I2P; auto-launches bundled i2pd (default: 1; -i2p=0 to disable)
-i2psam=<ip:port> SAM bridge address (default: 127.0.0.1:7656).
A non-loopback address disables the bundled router and
connects to that external bridge instead.
```
## Checking it
* GUI: the `.b32.i2p` address sits on top of the `.onion` in the status bar;
click either to copy.
* RPC: `getinfo` shows `toraddress` and `i2paddress`; `getnetworkinfo` shows
`toraddress` and an `i2p` object (`enabled`, `active`, `address`, `peers`).
## Notes / limitations
* The address serialization format carries a flag for I2P addresses, so **all
nodes must run this build** to exchange I2P peers; an old `peers.dat` is
discarded.
* `i2p_private_key` is your stable I2P identity — back it up, don't delete it.
* This was implemented without a build/CI environment here; build and test
against a real i2pd before relying on it.
+1 -1
View File
@@ -3,7 +3,7 @@
# Run on a Linux x64 system with appimagetool installed
set -e
VERSION="5.7.6"
VERSION="6.1.0"
APPDIR="Triangles-x86_64.AppDir"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
+1 -1
View File
@@ -3,7 +3,7 @@
# Run from the packaging/debian directory
set -e
VERSION="5.7.6"
VERSION="6.1.0"
PKGDIR="triangles_${VERSION}-1_amd64"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
+3 -3
View File
@@ -1,6 +1,6 @@
FROM ubuntu:22.04 AS builder
ARG VERSION=5.9.20
ARG VERSION=6.1.0
ARG DEB_URL=https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles-daemon_${VERSION}_amd64.deb
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -13,11 +13,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# ---------- Runtime ----------
FROM ubuntu:22.04
ARG VERSION=5.9.20
ARG VERSION=6.1.0
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="${VERSION}"
LABEL version="6.1.0"
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
trianglesd:
build: .
image: cryptographic-triangles/trianglesd:5.7.6
image: cryptographic-triangles/trianglesd:6.1.0
container_name: trianglesd
restart: unless-stopped
ports:
@@ -25,7 +25,7 @@ modules:
- install -Dm644 org.cryptographic_triangles.TrianglesQt.metainfo.xml /app/share/metainfo/org.cryptographic_triangles.TrianglesQt.metainfo.xml
sources:
- type: file
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-qt
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-qt
sha256: ed220eb8d0b403f62cdac28988541fd1a27864491e233216f9c00a4c2537b4a3
dest-filename: triangles-qt-linux
- type: file
@@ -55,6 +55,6 @@ modules:
- install -Dm755 trianglesd-linux /app/bin/trianglesd
sources:
- type: file
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-daemon
sha256: 4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517
dest-filename: trianglesd-linux
+1 -1
View File
@@ -4,7 +4,7 @@
# Install build tools: sudo dnf install rpm-build rpmdevtools
set -e
VERSION="5.7.6"
VERSION="6.1.0"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
echo "Building RPM for Triangles v${VERSION}..."
+1 -1
View File
@@ -1,5 +1,5 @@
Name: triangles
Version: 5.7.6
Version: 6.1.0
Release: 1%{?dist}
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
License: MIT
+2 -2
View File
@@ -1,11 +1,11 @@
{
"version": "5.7.6",
"version": "6.1.0",
"description": "Cryptographic Triangles (TRI) cryptocurrency wallet with PoS staking and encrypted messaging",
"homepage": "https://cryptographic-triangles.org",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip",
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-6.1.0-win-x64.zip",
"hash": "6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7"
}
},
@@ -1,5 +1,5 @@
PackageIdentifier: CryptographicTriangles.TrianglesQt
PackageVersion: 5.7.6
PackageVersion: 6.1.0
PackageLocale: en-US
Publisher: Cryptographic Triangles
PublisherUrl: https://cryptographic-triangles.org
@@ -27,7 +27,7 @@ Installers:
- RelativeFilePath: triangles-qt.exe
PortableCommandAlias: triangles-qt
ArchiveBinariesDependOnPath: true
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-6.1.0-win-x64.zip
InstallerSha256: 6F002A669A7E92AAF3D8DD7B1AE80F06A086C99A15CA05CF107665009FFC06B7
ManifestType: singleton
ManifestVersion: 1.6.0
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# build-rocksdb.sh — Build and install a pinned RocksDB version for CI.
#
# Ubuntu 22.04's librocksdb-dev is 6.11.4 (the same version that bit
# DNS2 — see PR #10). Triangles requires RocksDB >= 7.4.0 for the XXH3
# per-block checksum used in modern smsgDB SST files; src/smessage.cpp's
# SecMsgDB::Open has a runtime quarantine fallback, but the build-time
# check in CMakeLists.txt refuses to configure against < 7.4.
#
# This script clones RocksDB at a pinned tag, builds only the shared
# library (fast), installs to /usr/local, and refreshes ldconfig.
# Triangles' CMake find_library probes /usr/local before /usr/lib so
# the just-built copy is picked up first.
#
# Pinned version matches DNS2's system librocksdb (8.9.1) so test
# coverage matches production.
#
# Usage: sudo ./scripts/ci/build-rocksdb.sh
set -euo pipefail
ROCKSDB_VERSION="${ROCKSDB_VERSION:-8.9.1}"
ROCKSDB_TAG="v${ROCKSDB_VERSION}"
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}"
JOBS="${JOBS:-$(nproc)}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo ">>> Building RocksDB ${ROCKSDB_TAG} (${JOBS} jobs) into ${INSTALL_PREFIX}"
git clone --depth 1 --branch "${ROCKSDB_TAG}" \
https://github.com/facebook/rocksdb.git "${WORKDIR}/rocksdb"
cd "${WORKDIR}/rocksdb"
# Shared library only — Triangles links dynamically. Statically linking
# rocksdb.a would also work but balloons the daemon binary by ~50 MB.
make -j"${JOBS}" shared_lib PORTABLE=1 USE_RTTI=1 \
EXTRA_CXXFLAGS="-Wno-error=deprecated-declarations"
make install-shared PREFIX="${INSTALL_PREFIX}"
# Scrub the rocksdb.pc that install-shared just wrote. RocksDB's
# Makefile unconditionally appends `-isystem third-party/gtest-1.8.1/
# fused-src` to Cflags, which is a RELATIVE path baked in from the build
# directory. Modern CMake (>= 3.27) refuses to consume imported targets
# with non-existent relative paths in INTERFACE_INCLUDE_DIRECTORIES,
# so pkg_check_modules(rocksdb) on a Triangles configure errors out
# with: 'Imported target "PkgConfig::RocksDB" includes non-existent
# path "third-party/gtest-1.8.1/fused-src"'.
#
# Replace the bad flag with the absolute include dir so pkg-config
# consumers see a path that actually exists on disk.
PC_FILE="${INSTALL_PREFIX}/lib/pkgconfig/rocksdb.pc"
if [ -f "${PC_FILE}" ]; then
sed -i \
-e "s|-isystem third-party/gtest-1.8.1/fused-src|-I${INSTALL_PREFIX}/include|g" \
-e "s|-isystem \\\${prefix}/third-party/gtest-1.8.1/fused-src|-I${INSTALL_PREFIX}/include|g" \
-e 's|-std=c++17 ||g' \
-e 's|-std=c++17$||g' \
"${PC_FILE}"
fi
ldconfig
# Sanity: installed library should be on disk and registered with ldconfig.
# ldconfig strips the patch version from its output, so we check both:
# 1. File exists at the versioned path (definitive).
# 2. ldconfig shows a matching major.minor (sanity for runtime linker).
ROCKSDB_MAJOR_MINOR="${ROCKSDB_VERSION%.*}"
if [ ! -f "${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}" ]; then
echo "!!! librocksdb.so.${ROCKSDB_VERSION} not found at ${INSTALL_PREFIX}/lib/" >&2
ls -l "${INSTALL_PREFIX}/lib/librocksdb"* 2>&1 || true
exit 1
fi
if ! ldconfig -p | grep -q "librocksdb.so.${ROCKSDB_MAJOR_MINOR}"; then
echo "!!! ldconfig did not register librocksdb.so.${ROCKSDB_MAJOR_MINOR}" >&2
ldconfig -p | grep -i rocksdb >&2 || true
exit 1
fi
echo ">>> RocksDB ${ROCKSDB_TAG} installed to ${INSTALL_PREFIX}"
echo ">>> - library: ${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}"
echo ">>> - headers: ${INSTALL_PREFIX}/include/rocksdb/version.h"
ls -l "${INSTALL_PREFIX}/lib/librocksdb.so"* "${INSTALL_PREFIX}/include/rocksdb/version.h"
+5 -5
View File
@@ -1,6 +1,6 @@
name: triangles
base: core22
version: '5.7.6'
version: '6.1.0'
summary: Cryptographic Triangles (TRI) cryptocurrency wallet
description: |
Privacy-focused cryptocurrency featuring Proof-of-Stake consensus,
@@ -51,10 +51,10 @@ apps:
parts:
triangles:
plugin: dump
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-qt
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-qt
source-type: file
organize:
Cryptographic-Triangles-v5.7.6-linux-x64-qt: bin/triangles-qt
Cryptographic-Triangles-v6.1.0-linux-x64-qt: bin/triangles-qt
stage-packages:
- libqt5widgets5
- libqt5gui5
@@ -73,10 +73,10 @@ parts:
trianglesd:
plugin: dump
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-daemon
source-type: file
organize:
Cryptographic-Triangles-v5.7.6-linux-x64-daemon: bin/trianglesd
Cryptographic-Triangles-v6.1.0-linux-x64-daemon: bin/trianglesd
desktop-entry:
plugin: dump
+185 -6
View File
@@ -40,6 +40,7 @@ target_include_directories(json_compat INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/js
set(CORE_SOURCES
addrman.cpp
bootstrap.cpp
checkpointpublisher.cpp
checkpoints.cpp
crypter.cpp
hdwallet.cpp
@@ -85,6 +86,7 @@ set(CORE_SOURCES
tor/onion_v3.cpp
tor/tor_process.cpp
tor/tor_embedded.cpp
i2p/i2p_embedded.cpp
)
# Scrypt assembly — platform-specific
@@ -106,12 +108,22 @@ endif()
# for the rationale — RocksDB also backs the smessage store).
list(APPEND CORE_SOURCES txdb-rocksdb.cpp)
# Modernization: SQLite wallet DB backend + Berkeley→SQLite migration.
# Built unconditionally; selection happens at runtime via -walletdb.
list(APPEND CORE_SOURCES
walletdb-factory.cpp
walletdb-sqlite.cpp
walletdb-recover.cpp
walletmigrate.cpp
)
add_library(triangles_common OBJECT ${CORE_SOURCES})
target_include_directories(triangles_common PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/json"
"${CMAKE_CURRENT_SOURCE_DIR}/tor"
"${CMAKE_CURRENT_SOURCE_DIR}/i2p"
"${CMAKE_BINARY_DIR}/generated" # for build.h
)
@@ -123,13 +135,11 @@ target_link_libraries(triangles_common PUBLIC
leveldb_bundled
OpenSSL::SSL
OpenSSL::Crypto
Boost::program_options
Boost::thread
Boost::chrono
BerkeleyDB::BerkeleyDB
Libevent::Libevent
ZLIB::ZLIB
Threads::Threads
SQLite::SQLite3
)
# Optional: UPnP
@@ -178,19 +188,118 @@ if(USE_TOR_EMBEDDED)
# and its dependencies.
# Use --allow-multiple-definition because libtor.a may pull in static
# OpenSSL objects that duplicate the DLL import lib already linked above.
# These GNU ld options are not supported on macOS (which uses lld) —
# guard with NOT APPLE so the build still works on macOS.
# On macOS, the libevent/openssl/zlib install paths are not on the
# default linker search path. Pull them in from the standard
# homebrew locations so -levent / -lssl / -lssl etc. resolve.
if(APPLE)
target_link_directories(triangles_common PUBLIC
/opt/homebrew/opt/libevent/lib
/opt/homebrew/opt/openssl@3/lib
/opt/homebrew/opt/zlib/lib
)
endif()
if(NOT APPLE)
target_link_libraries(triangles_common PUBLIC
-Wl,--allow-multiple-definition
-Wl,--start-group
)
endif()
target_link_libraries(triangles_common PUBLIC
-Wl,--allow-multiple-definition
-Wl,--start-group
-ltor
-levent -levent_core -levent_extra -levent_openssl
-lssl -lcrypto -lz -llzma -lzstd
-Wl,--end-group
)
if(NOT APPLE)
target_link_libraries(triangles_common PUBLIC
-Wl,--end-group
)
endif()
if(WIN32)
target_link_libraries(triangles_common PUBLIC iphlpapi shlwapi crypt32)
endif()
endif()
# Optional: Embedded I2P (i2pd)
if(USE_I2P_EMBEDDED)
if(I2P_SOURCE_ROOT STREQUAL "")
set(I2P_SOURCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/i2p/i2pd-src")
endif()
if(NOT EXISTS "${I2P_SOURCE_ROOT}/libi2pd/Crypto.h")
message(FATAL_ERROR
"USE_I2P_EMBEDDED=ON but i2pd source not found at ${I2P_SOURCE_ROOT}.\n"
"Run: git submodule update --init --recursive\n"
"Or set -DI2P_SOURCE_ROOT=/path/to/i2pd")
endif()
target_compile_definitions(triangles_common PUBLIC ENABLE_I2P_EMBEDDED)
target_include_directories(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}"
"${I2P_SOURCE_ROOT}/libi2pd"
"${I2P_SOURCE_ROOT}/libi2pd_client"
"${I2P_SOURCE_ROOT}/i18n"
)
# i2pd builds as two static libraries: libi2pd.a (core router) and
# libi2pd_client.a (SAM, SOCKS, tunnels, client context). Both are needed.
# i2pd's own Makefile.mingw links by full static .a paths rather than
# -l flags because MinGW's linker is single-pass and CMake imported
# targets (Boost::) may not exist on MSYS2. We follow the same pattern:
# link the archives, then their Boost/zlib deps as full paths, then
# the archives again to resolve the second-pass references.
target_link_libraries(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}/libi2pdclient.a"
"${I2P_SOURCE_ROOT}/libi2pd.a"
"${I2P_SOURCE_ROOT}/libi2pdlang.a"
)
if(WIN32)
# MinGW/MSYS2: Boost:: CMake imported targets are unreliable here.
# Use find_library to locate the actual .a/.dll files. Some Boost
# libs (e.g. boost_system) are header-only in newer versions and
# won't have a .a file at all — that's fine, we skip them.
if(NOT MINGW_PREFIX)
if(DEFINED ENV{MINGW_PREFIX})
set(MINGW_PREFIX "$ENV{MINGW_PREFIX}")
else()
set(MINGW_PREFIX "/mingw64")
endif()
endif()
find_library(I2P_BOOST_FS NAMES boost_filesystem-mt boost_filesystem libboost_filesystem-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_BOOST_PO NAMES boost_program_options-mt boost_program_options libboost_program_options-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_BOOST_SYS NAMES boost_system-mt boost_system libboost_system-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_SSL NAMES ssl libssl HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_CRYPTO NAMES crypto libcrypto HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_Z NAMES z libz zlib HINTS "${MINGW_PREFIX}/lib")
set(I2P_WIN_LIBS "")
foreach(lib I2P_BOOST_FS I2P_BOOST_PO I2P_BOOST_SYS I2P_SSL I2P_CRYPTO I2P_Z)
if(${lib})
list(APPEND I2P_WIN_LIBS "${${lib}}")
message(STATUS " I2P link: ${lib} = ${${lib}}")
else()
message(STATUS " I2P link: ${lib} = (not found, header-only?)")
endif()
endforeach()
target_link_libraries(triangles_common PUBLIC ${I2P_WIN_LIBS} -Wl,--allow-multiple-definition)
else()
target_link_libraries(triangles_common PUBLIC
Boost::program_options Boost::thread Boost::chrono
OpenSSL::SSL OpenSSL::Crypto
ZLIB::ZLIB
)
if(TARGET Boost::filesystem)
target_link_libraries(triangles_common PUBLIC Boost::filesystem)
endif()
if(TARGET Boost::system)
target_link_libraries(triangles_common PUBLIC Boost::system)
endif()
endif()
# Second pass: list archives again so linker resolves i2pd→Boost refs
# that were unsatisfied in the first left-to-right pass.
target_link_libraries(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}/libi2pd.a"
"${I2P_SOURCE_ROOT}/libi2pdclient.a"
)
endif()
# Platform-specific libraries
if(WIN32)
target_link_libraries(triangles_common PUBLIC
@@ -420,6 +529,7 @@ if(BUILD_QT)
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Network
)
# Optional: D-Bus notifications (Linux)
@@ -483,6 +593,9 @@ if(BUILD_TESTS)
file(GLOB TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test/*.cpp")
# Exclude miner_tests.cpp (never ported from Bitcoin)
list(FILTER TEST_SOURCES EXCLUDE REGEX "miner_tests\\.cpp$")
# Exclude the standalone chaindb test driver — it gets its own target
# because it needs to run without the TestingSetup global fixture.
list(FILTER TEST_SOURCES EXCLUDE REGEX "chaindb_equivalence_tests_main\\.cpp$")
add_executable(test_triangles
${TEST_SOURCES}
@@ -507,4 +620,70 @@ if(BUILD_TESTS)
)
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
# ── Standalone chaindb equivalence tests ─────────────────────────────────
# Runs without the TestingSetup global fixture (which would otherwise
# open the real chain DB and lock it for the process). Sets a fresh
# temp -datadir via its own global fixture, then runs the
# chaindb_equivalence_tests suite.
add_executable(test_chaindb_equivalence
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_equivalence_tests_main.cpp"
# wallet.cpp provides the CWallet symbols that triangles_common
# (txdb-rocksdb, net, etc.) references, even though the chaindb
# tests themselves don't use the wallet.
wallet.cpp
)
target_include_directories(test_chaindb_equivalence PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_equivalence PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_equivalence_tests
COMMAND test_chaindb_equivalence --log_level=test_suite)
# ── Standalone snapshotnet P2P tests ────────────────────────────────────
# Same rationale as test_chaindb_equivalence: snapshotnet needs filesystem
# and threading globals and its own tmp datadir fixture, which would
# conflict with test_triangles' heavy TestingSetup. Runs independently.
add_executable(test_snapshotnet
"${CMAKE_CURRENT_SOURCE_DIR}/test/snapshotnet_tests.cpp"
wallet.cpp
)
target_include_directories(test_snapshotnet PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_snapshotnet PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME snapshotnet_tests
COMMAND test_snapshotnet --log_level=test_suite)
# ── Standalone chaindb runtime tests (CRocksTxDB wrapper layer) ─────────
# Exercises MakeChainDB / WipeChainDataDir / IsRocksDbChainBackend and
# the CRocksTxDB write/read/batch/iterator wrapper — the same code path
# the daemon uses when launched with `-chaindb=rocksdb`. The
# chaindb_equivalence_tests (above) only verify the byte-copy migration
# via the raw leveldb/rocksdb APIs; this one verifies the wrapper class.
add_executable(test_chaindb_runtime
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_runtime_tests.cpp"
wallet.cpp
)
target_include_directories(test_chaindb_runtime PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_runtime PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_runtime_tests
COMMAND test_chaindb_runtime --log_level=test_suite)
endif()
+92
View File
@@ -504,6 +504,8 @@ bool ParseManifest(const fs::path& manifestPath,
manifest.hash = val;
else if (key == "dbversion")
manifest.dbversion = std::atoi(val.c_str());
else if (key == "signature")
manifest.signature = val;
}
in.close();
@@ -566,6 +568,96 @@ bool VerifyManifest(const SnapshotManifest& manifest,
return false;
}
// ─── Signature verification (#11) ─────────────────────────────────────
// If the manifest includes a signature, verify it against the
// compiled-in snapshot signing key. This prevents MITM attacks
// where an attacker replaces the snapshot file on the bootstrap server.
//
// If no signature is present, print a warning but continue (backward
// compatibility with older snapshots that pre-date signing).
if (!manifest.signature.empty()) {
// Build the message that was signed: "height||hash" (ASCII)
std::string message = std::to_string(manifest.height) + "||" + manifest.hash;
// Decode the hex-encoded signature (64 bytes for Ed25519)
std::vector<unsigned char> sigBytes;
if (manifest.signature.size() != 128) { // 64 bytes hex = 128 chars
strError = "Invalid signature length in manifest (expected 128 hex chars, got "
+ std::to_string(manifest.signature.size()) + ")";
return false;
}
for (size_t i = 0; i < manifest.signature.size(); i += 2) {
auto hexVal = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
int hi = hexVal(manifest.signature[i]);
int lo = hexVal(manifest.signature[i + 1]);
if (hi < 0 || lo < 0) {
strError = "Invalid hex in manifest signature";
return false;
}
sigBytes.push_back((hi << 4) | lo);
}
// Snapshot signing public key (Ed25519, 32 bytes).
// This is the public half of the key used to sign snapshots on the
// bootstrap server. The private key never leaves the build machine.
// To rotate: generate new keypair, update this constant, re-sign
// all snapshots, update manifest files.
static const unsigned char snapshotPubkey[32] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}; // Placeholder: replace with actual pubkey when signing is deployed
// Use OpenSSL Ed25519 verification
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) {
strError = "Failed to allocate EVP context for signature verification";
return false;
}
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr,
snapshotPubkey, 32);
if (!pkey) {
EVP_MD_CTX_free(mdctx);
strError = "Failed to load snapshot signing public key";
return false;
}
int rc = EVP_DigestVerifyInit(mdctx, nullptr, nullptr, nullptr, pkey);
if (rc != 1) {
EVP_PKEY_free(pkey);
EVP_MD_CTX_free(mdctx);
strError = "Failed to init signature verification";
return false;
}
rc = EVP_DigestVerify(mdctx,
sigBytes.data(), sigBytes.size(),
(const unsigned char*)message.data(), message.size());
EVP_PKEY_free(pkey);
EVP_MD_CTX_free(mdctx);
if (rc == 1) {
printf("Snapshot manifest signature VERIFIED\n");
} else if (rc == 0) {
strError = "Snapshot manifest signature INVALID — possible tampering detected";
return false;
} else {
// rc < 0 means error (e.g., placeholder zero pubkey not yet deployed)
printf("WARNING: Snapshot manifest signature verification error (rc=%d). "
"Signing key may not be deployed yet. Proceeding without verification.\n", rc);
}
} else {
printf("WARNING: Snapshot manifest has no signature — loading WITHOUT signature verification\n");
}
return true;
}
+1
View File
@@ -53,6 +53,7 @@ namespace Bootstrap {
int height; // block height of the snapshot tip
std::string hash; // block hash at that height (hex, no 0x prefix)
int dbversion; // DATABASE_VERSION the txleveldb was built with
std::string signature; // Ed25519 signature of (height || hash), hex-encoded (empty if unsigned)
};
// Parse a snapshot.manifest file into a SnapshotManifest struct.
+473
View File
@@ -0,0 +1,473 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24) — implementation.
//
// See checkpointpublisher.h for the design. This file holds:
// - The in-memory signed-checkpoint cache (a CCriticalSection-guarded
// std::map keyed by height; values are block hashes)
// - The canonical serialization used by both producer and consumer
// - The JSON parsing/building helpers (small subset, no third-party deps)
// - The trusted signers list (mirrors IsTrustedSnapshotSigner)
#include "checkpointpublisher.h"
#include <algorithm>
#include <cstdio>
#include <map>
#include <set>
#include <sstream>
#include <vector>
#include "sync.h"
#include "util.h"
#include "base58.h"
#include "key.h"
#include "serialize.h"
#include "net.h" // for CCriticalSection
#include "main.h" // for strMessageMagic
#include "bootstrap.h" // for Bootstrap::DownloadFile
namespace Checkpoints {
// ============================================================================
// Trusted signers
// ============================================================================
//
// Mirrors Bootstrap::TRUSTED_SNAPSHOT_SIGNERS but kept SEPARATE so the two
// lists can be managed independently. The default trust list contains the
// project operator's address. Operators can extend via a future -trustedcheckpointsigner
// conf option (not yet implemented — see Phase 2 in checkpointpublisher.h).
static const char* TRUSTED_CHECKPOINT_SIGNERS[] = {
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's wallet (DNS2 default)
};
static const size_t NUM_TRUSTED_CHECKPOINT_SIGNERS =
sizeof(TRUSTED_CHECKPOINT_SIGNERS) / sizeof(TRUSTED_CHECKPOINT_SIGNERS[0]);
bool IsTrustedCheckpointSigner(const std::string& addr)
{
for (size_t i = 0; i < NUM_TRUSTED_CHECKPOINT_SIGNERS; ++i) {
if (addr == TRUSTED_CHECKPOINT_SIGNERS[i]) return true;
}
return false;
}
// ============================================================================
// In-memory cache of loaded signed checkpoints
// ============================================================================
//
// Guarded by a single CCriticalSection. The cache is small (a few thousand
// entries max — operator publishes one every N=5000 blocks, so for a 2.2M
// chain that's ~440 entries per active signer). Lookup is O(log n).
static CCriticalSection cs_signedCheckpoints;
static std::map<int, std::string> mapSignedCheckpoints;
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex)
{
LOCK(cs_signedCheckpoints);
auto it = mapSignedCheckpoints.find(nHeight);
if (it == mapSignedCheckpoints.end()) return false;
// case-insensitive compare — JSON parsers sometimes downcase hex
if (it->second.size() != hashHex.size()) return false;
for (size_t i = 0; i < it->second.size(); i++) {
if (std::tolower(static_cast<unsigned char>(it->second[i])) !=
std::tolower(static_cast<unsigned char>(hashHex[i]))) {
return false;
}
}
return true;
}
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries)
{
LOCK(cs_signedCheckpoints);
for (const auto& e : entries) {
// Don't overwrite compiled-in mapCheckpoints — that gate runs FIRST
// in AcceptBlock. The signed set is a SUPPLEMENT, not a replacement.
mapSignedCheckpoints[e.nHeight] = e.hashHex;
}
printf("Checkpoints: added %lu signed-remote checkpoints to cache\n", (unsigned long)entries.size());
}
void ClearSignedCheckpoints()
{
LOCK(cs_signedCheckpoints);
mapSignedCheckpoints.clear();
}
// ============================================================================
// Canonical serialization — producer + consumer MUST agree on this byte sequence
// ============================================================================
//
// Format: "<height1>:<hash1>:<ts1>;<height2>:<hash2>:<ts2>;..."
//
// Properties:
// - Entries in DESCENDING order (tip first)
// - Lowercase hex, no 0x prefix, no leading zeros
// - Timestamps are unix seconds, decimal
// - Field separator ':' — guaranteed not to appear in hex
// - Entry separator ';' — guaranteed not to appear in either
// - Trailing newline is NOT part of the signed payload (producers MUST NOT
// add one to the message before signing; consumers MUST NOT trim it off
// the fetched JSON's message field before verifying)
//
// This function is PURE — no I/O, no globals. Tested in checkpoint_tests.cpp.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries)
{
std::string out;
for (size_t i = 0; i < entries.size(); i++) {
if (i > 0) out += ";";
out += std::to_string(entries[i].nHeight);
out += ":";
out += entries[i].hashHex;
out += ":";
out += std::to_string(entries[i].nTimestamp);
}
return out;
}
// ============================================================================
// Producer — build the JSON document
// ============================================================================
//
// This is intentionally a thin wrapper: the wallet signing happens in the
// caller (rpcwallet.cpp / daemon loop), which has the unlocked key. Here we
// just escape + format.
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError)
{
if (entries.empty()) {
strError = "BuildSignedCheckpointsJson: entries vector is empty";
return false;
}
if (signingAddress.empty()) {
strError = "BuildSignedCheckpointsJson: signingAddress is empty";
return false;
}
if (signatureBase64.empty()) {
strError = "BuildSignedCheckpointsJson: signature is empty";
return false;
}
// Sort entries DESCENDING by height — canonical form. Producers and
// consumers both depend on this so verification is deterministic.
std::vector<SignedCheckpoint> sorted = entries;
std::sort(sorted.begin(), sorted.end(),
[](const SignedCheckpoint& a, const SignedCheckpoint& b) {
return a.nHeight > b.nHeight;
});
// Build JSON manually — no third-party deps. Format is intentionally
// simple (no nested objects beyond the entries array).
std::ostringstream oss;
oss << "{\n";
oss << " \"format_version\": 1,\n";
oss << " \"signing_address\": \"" << signingAddress << "\",\n";
oss << " \"message\": \"" << message << "\",\n";
oss << " \"signature\": \"" << signatureBase64 << "\",\n";
oss << " \"entries\": [\n";
for (size_t i = 0; i < sorted.size(); i++) {
oss << " {\"height\": " << sorted[i].nHeight
<< ", \"hash\": \"" << sorted[i].hashHex << "\""
<< ", \"timestamp\": " << sorted[i].nTimestamp << "}";
if (i + 1 < sorted.size()) oss << ",";
oss << "\n";
}
oss << " ]\n";
oss << "}\n";
outJson = oss.str();
return true;
}
// ============================================================================
// Consumer — verify a JSON document
// ============================================================================
// Small JSON helper — extract a top-level array of objects from the
// "entries" field. We don't need full JSON parsing; the format is fixed.
static std::vector<std::string> ExtractJsonObjectArray(
const std::string& json, const std::string& field)
{
std::vector<std::string> objs;
std::string key = "\"" + field + "\"";
size_t pos = json.find(key);
if (pos == std::string::npos) return objs;
pos += key.size();
while (pos < json.size() && (json[pos] == ' ' || json[pos] == ':' ||
json[pos] == '\t' || json[pos] == '\n' || json[pos] == '\r'))
pos++;
if (pos >= json.size() || json[pos] != '[') return objs;
pos++; // past '['
while (pos < json.size()) {
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||
json[pos] == '\n' || json[pos] == '\r' || json[pos] == ','))
pos++;
if (pos >= json.size() || json[pos] == ']') break;
if (json[pos] != '{') break;
// Find matching closing brace (shallow — no nested objects in entries)
int depth = 1;
size_t start = pos;
pos++;
while (pos < json.size() && depth > 0) {
if (json[pos] == '{') depth++;
else if (json[pos] == '}') depth--;
pos++;
}
if (depth != 0) break;
objs.push_back(json.substr(start, pos - start));
}
return objs;
}
// Extract an integer field from an entry object like:
// {"height": 12345, "hash": "...", "timestamp": 1700000000}
static int ExtractJsonInt(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return 0;
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
// Parse a non-negative integer
int n = 0;
bool foundAny = false;
while (pos < obj.size() && obj[pos] >= '0' && obj[pos] <= '9') {
n = n * 10 + (obj[pos] - '0');
pos++;
foundAny = true;
}
if (!foundAny) return 0;
return n;
}
// Extract a string field from a small JSON object — mirrors ExtractJsonString
// in bootstrap.cpp. Duplicated here to keep checkpointpublisher.cpp standalone
// (no link dependency on bootstrap.cpp internals).
static std::string ExtractJsonString(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return "";
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
if (pos >= obj.size() || obj[pos] != '\"') return "";
pos++;
size_t end = obj.find('\"', pos);
if (end == std::string::npos) return "";
return obj.substr(pos, end - pos);
}
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
// 1. Extract signing fields
outSigningAddress = ExtractJsonString(jsonText, "signing_address");
std::string signature = ExtractJsonString(jsonText, "signature");
std::string message = ExtractJsonString(jsonText, "message");
if (outSigningAddress.empty() || signature.empty() || message.empty()) {
strError = "signed-checkpoints JSON missing required top-level fields "
"(signing_address/signature/message)";
return false;
}
// 2. Verify signer is trusted
if (!IsTrustedCheckpointSigner(outSigningAddress)) {
strError = "signing_address " + outSigningAddress +
" is not in the trusted checkpoint signers list";
return false;
}
// 3. Verify the address is well-formed (catches typos early)
CTrianglesAddress addr(outSigningAddress);
if (!addr.IsValid()) {
strError = "signing_address " + outSigningAddress + " is not a valid Triangles address";
return false;
}
CKeyID keyID;
if (!addr.GetKeyID(keyID)) {
strError = "signing_address " + outSigningAddress + " does not refer to a key";
return false;
}
// 4. Decode and verify the signature (same code path as verifymessage RPC)
bool fInvalid = false;
std::vector<unsigned char> vchSig = DecodeBase64(signature.c_str(), &fInvalid);
if (fInvalid) {
strError = "signed-checkpoints signature is not valid base64";
return false;
}
CDataStream ss(SER_GETHASH, 0);
ss << strMessageMagic;
ss << message;
CKey key;
if (!key.SetCompactSignature(Hash(ss.begin(), ss.end()), vchSig)) {
strError = "signed-checkpoints signature failed to recover (bad sig or "
"message tampered)";
return false;
}
if (key.GetPubKey().GetID() != keyID) {
strError = "signed-checkpoints signature recovered to a key that does "
"not match the claimed signer address";
return false;
}
// 5. Extract entries and verify they match the signed message
std::vector<std::string> entryObjs = ExtractJsonObjectArray(jsonText, "entries");
if (entryObjs.empty()) {
strError = "signed-checkpoints JSON has no entries array or entries is empty";
return false;
}
outEntries.reserve(entryObjs.size());
for (const auto& obj : entryObjs) {
SignedCheckpoint e;
e.nHeight = ExtractJsonInt(obj, "height");
e.hashHex = ExtractJsonString(obj, "hash");
e.nTimestamp = ExtractJsonInt(obj, "timestamp");
if (e.nHeight <= 0 || e.hashHex.empty() || e.nTimestamp <= 0) {
strError = "malformed entry (height/hash/timestamp invalid): " + obj;
return false;
}
// hashHex sanity: must be exactly 64 lowercase hex chars
if (e.hashHex.size() != 64) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" is not 64 chars: " + e.hashHex;
return false;
}
for (char c : e.hashHex) {
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" contains non-lowercase-hex character";
return false;
}
}
outEntries.push_back(e);
}
// 6. Verify the signed message exactly matches the canonical serialization
// of the entries. This is the cross-check that proves the entries
// weren't tampered with after signing.
std::string expectedMessage = SerializeEntriesForSigning(outEntries);
if (expectedMessage != message) {
strError = "signed-checkpoints message does not match canonical entry "
"serialization — entries were tampered with after signing";
return false;
}
printf("Checkpoints: signed-remote verified — %lu entries signed by %s\n",
(unsigned long)outEntries.size(), outSigningAddress.c_str());
return true;
}
// ============================================================================
// Network fetch — keep it simple. The signed-checkpoints doc is tiny (~5 KB
// for a year of entries at 5000-block intervals), so a plain HTTP GET is
// fine. We DO NOT go through Tor for this fetch: the bootstrap server is
// already a known clearnet endpoint (same model as the existing UTXO
// snapshot download, which uses ConnectDirectTCP per bootstrap.cpp).
// ============================================================================
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
std::string jsonText;
// Path A: use on-disk copy if it exists (lets the daemon start even when
// the bootstrap server is unreachable, as long as we have a recent copy).
if (!onDiskPath.empty()) {
FILE* f = fopen(onDiskPath.c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) { // 10 MB cap — sanity
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
if (!jsonText.empty()) {
printf("Checkpoints: loaded on-disk signed-checkpoints from %s (%lu bytes)\n",
onDiskPath.c_str(), (unsigned long)jsonText.size());
}
}
}
// Path B: fetch from bootstrap server. We always try this — if it
// succeeds, prefer the freshest doc over the on-disk copy.
if (host.empty()) {
strError = "LoadSignedCheckpoints: no host provided and no on-disk copy found";
return !jsonText.empty(); // if we have disk content, still try to verify it
}
// Use Bootstrap::DownloadFile — already handles clearnet HTTPS, timeouts,
// and redirects. We do NOT proxy through Tor.
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
nullptr, strError,
/*noProxy=*/true)) {
std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp";
FILE* f = fopen(tmp.string().c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) {
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
}
std::error_code ec;
std::filesystem::remove(tmp, ec);
if (!jsonText.empty()) {
printf("Checkpoints: fetched fresh signed-checkpoints from %s (%lu bytes)\n",
host.c_str(), (unsigned long)jsonText.size());
// Persist to disk for next startup (only if onDiskPath was given)
if (!onDiskPath.empty()) {
FILE* f2 = fopen(onDiskPath.c_str(), "wb");
if (f2) {
fwrite(jsonText.data(), 1, (unsigned long)jsonText.size(), f2);
fclose(f2);
printf("Checkpoints: persisted signed-checkpoints to %s\n", onDiskPath.c_str());
}
}
}
} else {
printf("Checkpoints: WARNING — fetch from %s failed (%s)",
host.c_str(), strError.c_str());
if (jsonText.empty()) {
strError = "could not fetch signed-checkpoints and no on-disk copy: " + strError;
return false;
}
printf(" — falling back to on-disk copy\n");
strError.clear();
}
// Verify whatever we ended up with
return VerifySignedCheckpoints(jsonText, outEntries, outSigningAddress, strError);
}
} // namespace Checkpoints
+164
View File
@@ -0,0 +1,164 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24)
//
// Background
// ----------
// Triangles' existing CSyncCheckpoint (src/checkpoints.cpp) is Bitcoin-era
// P2P-broadcast code that uses a HARDCODED master pubkey. That model does
// not match how the project actually operates today (one operator with
// multiple keys, snapshot publishing on the bootstrap server, no master
// hierarchy). Instead we layer a *new* signed-checkpoint scheme on top of
// the bootstrap server, using the same compact-message primitive the UTXO
// snapshot trust model already uses (see src/bootstrap.cpp:IsTrustedSnapshotSigner).
//
// Trust model
// -----------
// - A signed checkpoint document is a small JSON file hosted at
// https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json
// - It contains a list of (height, block_hash, unix_timestamp) entries,
// followed by a single signing_address + signature covering the canonical
// serialization of the entry list.
// - The signing_address must appear in the trusted signers list
// (Checkpoints::IsTrustedCheckpointSigner, see checkpoints.cpp). The
// default trust list is the same as IsTrustedSnapshotSigner but kept
// separate so they can be managed independently.
// - Verification uses the existing CKey::SignCompact / SetCompactSignature
// code path through the wallet's verifymessage-style flow — no new
// cryptography is introduced.
//
// Producer
// --------
// - The daemon operator runs `triangles-cli publishcheckpoint [interval]`
// which builds the entry list from pindexBest, signs with the wallet's
// default key, and writes the JSON document to a path the operator
// uploads to the bootstrap server (or a cron job uploads automatically
// when -autopublishcheckpoint is set).
// - Default interval = every 5000 blocks; can be set to every N.
// - The first entry is always the chain tip at publish time.
//
// Consumer
// --------
// - On startup, the daemon can call
// Checkpoints::LoadSignedCheckpoints(host, dataDir, strError)
// which fetches, verifies, and merges the trusted entries into the
// compiled-in mapCheckpoints (lower priority — compiled-in wins on
// conflict to defend against remote-rollback).
// - Checkpoints::IsKnownSignedCheckpoint(height, hash) returns true if
// either compiled-in OR signed-remote knows about (height, hash).
//
// Relationship to existing code
// -----------------------------
// - mapCheckpoints in src/checkpoints.cpp is UNCHANGED — the compiled-in
// list is still the primary trust anchor.
// - Signed checkpoints EXTEND the trust anchor with operator-published
// ones, useful when the operator wants to publish a checkpoint at
// height 2,210,000 without waiting for a code release.
// - mapSnapshotHashes is unaffected.
#ifndef TRIANGLES_CHECKPOINT_PUBLISHER_H
#define TRIANGLES_CHECKPOINT_PUBLISHER_H
#include <string>
#include <vector>
#include <cstdint>
namespace Checkpoints {
// One signed checkpoint entry. Compact, serializable, no JSON inside the
// struct — JSON wrapping happens in the publisher.
struct SignedCheckpoint {
int nHeight; // block height
std::string hashHex; // block hash, lowercase hex, NO 0x prefix, NO leading zeros
int64_t nTimestamp; // unix seconds when published (signed over)
};
// Result of a publish or verify operation. Used for human-readable errors
// and structured logging.
struct SignedCheckpointResult {
bool ok; // overall success
std::string error; // populated if !ok
int nEntriesWritten; // for publish: how many entries went into the JSON
int nEntriesVerified; // for verify: how many entries passed signature check
};
// Default URL for the bootstrap server's signed-checkpoints document.
static const char* SIGNED_CHECKPOINTS_URL =
"https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json";
// Default local output path the daemon writes to on publish.
static const char* SIGNED_CHECKPOINTS_DEFAULT_OUT =
"/var/www/triangles-bootstrap/signed-checkpoints.json";
// ---- Producer ----
// Build the JSON document for the entries [heights[0], heights[1], ...]
// (in DESCENDING order — tip first) using the wallet's default key.
// Returns true on success; outJson/outputPath written. Wallet must be
// unlocked (signmessage requires it).
//
// This is the in-process builder used by both:
// - The triangles-cli `publishcheckpoint` RPC command
// - The daemon's auto-publish loop when -autopublishcheckpoint is set
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError);
// Canonical (deterministic) serialization of the entry list. The signature
// is over this exact byte sequence — both producer and consumer MUST use
// this function so verification is reproducible across platforms.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries);
// ---- Consumer ----
// Fetch the signed-checkpoints document from the bootstrap server, parse
// it, verify the signature, and return the verified entries. Does NOT
// merge into mapCheckpoints — caller decides what to do with the entries.
//
// onDiskPath: optional. If non-empty and the file already exists locally,
// skip the network fetch and verify the on-disk copy. This makes startup
// robust against bootstrap-server outages.
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Verify the signature on a parsed JSON document. Pure function — no
// network, no filesystem.
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Is the given signing address in the trusted signers list? Mirrors
// Bootstrap::IsTrustedSnapshotSigner but kept separate for independent
// governance.
bool IsTrustedCheckpointSigner(const std::string& addr);
// ---- Merged lookup ----
// Is (height, hash) known to either the compiled-in OR the
// signed-remote set? This is what AcceptBlock / fork-detection should call.
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex);
// Inject loaded entries into the in-memory signed-checkpoint cache. Called
// by init.cpp after LoadSignedCheckpoints returns successfully. Subsequent
// IsKnownSignedCheckpoint() calls will return true for any (height, hash)
// in the loaded set.
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries);
// Clear the in-memory cache (used at reorg boundaries and in tests).
void ClearSignedCheckpoints();
} // namespace Checkpoints
#endif // TRIANGLES_CHECKPOINT_PUBLISHER_H
+468 -453
View File
@@ -1,453 +1,468 @@
// Copyright (c) 2009-2012 The Bitcoin developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "checkpoints.h"
#include "txdb.h"
#include "main.h"
#include "uint256.h"
namespace Checkpoints
{
typedef std::map<int, uint256> MapCheckpoints;
//
// What makes a good checkpoint block?
// + Is surrounded by blocks with reasonable timestamps
// (no blocks before with a timestamp after, none after with
// timestamp before)
// + Contains no strange transactions
//
static MapCheckpoints mapCheckpoints = {
{ 0, hashGenesisBlockOfficial },
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
// Recent finality pin (PoS era). Closes the long unchecked span from
// 17650 to the live tip so stale-bootstrap / low-trust forks below
// this height are rejected outright. Hash from the canonical chain.
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
// UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify
// P2P-delivered snapshots without trusting any peer.
//
// Maintainers: after producing a snapshot, sha256 the file and add an entry
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
};
static MapCheckpoints mapCheckpointsTestnet = {
{ 0, hashGenesisBlockTestNet },
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
};
bool CheckHardened(int nHeight, const uint256& hash)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
if (i == checkpoints.end()) return true;
return hash == i->second;
}
bool IsKnownCheckpoint(int nHeight, const uint256& hash)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
if (i == checkpoints.end()) return false;
return hash == i->second;
}
int GetTotalBlocksEstimate()
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
return checkpoints.rbegin()->first;
}
int GetBestSnapshotHeight()
{
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
if (snaps.empty()) return 0;
return snaps.rbegin()->first;
}
bool GetSnapshotHash(int nHeight, uint256& fileHashOut)
{
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
auto it = snaps.find(nHeight);
if (it == snaps.end()) return false;
fileHashOut = it->second;
return true;
}
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it)
{
const uint256& hash = it->second;
std::map<uint256, CBlockIndex*>::const_iterator t = mapBlockIndex.find(hash);
if (t != mapBlockIndex.end())
return t->second;
}
return nullptr;
}
// triangles: synchronized checkpoint (centrally broadcasted)
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
CSyncCheckpoint checkpointMessage;
CSyncCheckpoint checkpointMessagePending;
uint256 hashInvalidCheckpoint = 0;
CCriticalSection cs_hashSyncCheckpoint;
// triangles: get last synchronized checkpoint
CBlockIndex* GetLastSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashSyncCheckpoint))
error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
else
return mapBlockIndex[hashSyncCheckpoint];
return nullptr;
}
// triangles: only descendant of current sync-checkpoint is allowed
bool ValidateSyncCheckpoint(uint256 hashCheckpoint)
{
if (!mapBlockIndex.count(hashSyncCheckpoint))
return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
if (!mapBlockIndex.count(hashCheckpoint))
return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str());
CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint];
CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint];
if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight)
{
// Received an older checkpoint, trace back from current checkpoint
// to the same height of the received checkpoint to verify
// that current checkpoint should be a descendant block
CBlockIndex* pindex = pindexSyncCheckpoint;
while (pindex->nHeight > pindexCheckpointRecv->nHeight)
if (!(pindex = pindex->pprev))
return error("ValidateSyncCheckpoint: pprev null - block index structure failure");
if (pindex->GetBlockHash() != hashCheckpoint)
{
hashInvalidCheckpoint = hashCheckpoint;
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
}
return false; // ignore older checkpoint
}
// Received checkpoint should be a descendant block of the current
// checkpoint. Trace back to the same height of current checkpoint
// to verify.
CBlockIndex* pindex = pindexCheckpointRecv;
while (pindex->nHeight > pindexSyncCheckpoint->nHeight)
if (!(pindex = pindex->pprev))
return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure");
if (pindex->GetBlockHash() != hashSyncCheckpoint)
{
hashInvalidCheckpoint = hashCheckpoint;
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
}
return true;
}
bool WriteSyncCheckpoint(const uint256& hashCheckpoint)
{
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
txdb.TxnBegin();
if (!txdb.WriteSyncCheckpoint(hashCheckpoint))
{
txdb.TxnAbort();
return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
}
if (!txdb.TxnCommit())
return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
Checkpoints::hashSyncCheckpoint = hashCheckpoint;
return true;
}
bool AcceptPendingSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint))
{
if (!ValidateSyncCheckpoint(hashPendingCheckpoint))
{
hashPendingCheckpoint = 0;
checkpointMessagePending.SetNull();
return false;
}
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint];
if (!pindexCheckpoint->IsInMainChain())
{
CBlock block;
if (!block.ReadFromDisk(pindexCheckpoint))
return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
if (!block.SetBestChain(txdb, pindexCheckpoint))
{
hashInvalidCheckpoint = hashPendingCheckpoint;
return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
}
}
if (!WriteSyncCheckpoint(hashPendingCheckpoint))
return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
hashPendingCheckpoint = 0;
checkpointMessage = checkpointMessagePending;
checkpointMessagePending.SetNull();
printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str());
// relay the checkpoint
if (!checkpointMessage.IsNull())
{
for (CNode* pnode : vNodes)
checkpointMessage.RelayTo(pnode);
}
return true;
}
return false;
}
// Automatically select a suitable sync-checkpoint
uint256 AutoSelectSyncCheckpoint()
{
const CBlockIndex *pindex = pindexBest;
// Search backward for a block within max span and maturity window
while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight))
pindex = pindex->pprev;
return pindex->GetBlockHash();
}
// Check against synchronized checkpoint
// Disabled: master key removed in V5, no new sync checkpoints possible.
// Always returns true to prevent stale DB-persisted checkpoints from blocking IBD.
bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev)
{
return true;
}
bool WantedByPendingSyncCheckpoint(uint256 hashBlock)
{
LOCK(cs_hashSyncCheckpoint);
if (hashPendingCheckpoint == 0)
return false;
if (hashBlock == hashPendingCheckpoint)
return true;
if (mapOrphanBlocks.count(hashPendingCheckpoint)
&& hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get()))
return true;
return false;
}
// triangles: reset synchronized checkpoint to last hardened checkpoint
bool ResetSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
const uint256& hash = mapCheckpoints.rbegin()->second;
if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain())
{
// checkpoint block accepted but not yet in main chain
printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str());
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlock block;
if (!block.ReadFromDisk(mapBlockIndex[hash]))
return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str());
if (!block.SetBestChain(txdb, mapBlockIndex[hash]))
{
return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str());
}
}
else if(!mapBlockIndex.count(hash))
{
// checkpoint block not yet accepted
hashPendingCheckpoint = hash;
checkpointMessagePending.SetNull();
printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str());
}
for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it)
{
const uint256& hash = it->second;
if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain())
{
if (!WriteSyncCheckpoint(hash))
return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str());
printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str());
return true;
}
}
return false;
}
void AskForPendingSyncCheckpoint(CNode* pfrom)
{
LOCK(cs_hashSyncCheckpoint);
if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint)))
pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint));
}
bool SetCheckpointPrivKey(std::string strPrivKey)
{
// Test signing a sync-checkpoint with genesis block
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return false;
// Test signing successful, proceed
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
return true;
}
bool SendSyncCheckpoint(uint256 hashCheckpoint)
{
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = hashCheckpoint;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
if (CSyncCheckpoint::strMasterPrivKey.empty())
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
{
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
return false;
}
// Relay checkpoint
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
checkpoint.RelayTo(pnode);
}
return true;
}
// Is the sync-checkpoint outside maturity window?
bool IsMatureSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashSyncCheckpoint))
return true; // no valid sync checkpoint, treat as mature
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity ||
pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime());
}
}
// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork)
const std::string CSyncCheckpoint::strMasterPubKey = "";
std::string CSyncCheckpoint::strMasterPrivKey = "";
// triangles: verify signature of sync-checkpoint message
// Master key system disabled - checkpoint signatures are no longer required
bool CSyncCheckpoint::CheckSignature()
{
// Deserialize the checkpoint data without signature verification
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
sMsg >> *(CUnsignedSyncCheckpoint*)this;
return true;
}
// triangles: process synchronized checkpoint
bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom)
{
if (!CheckSignature())
return false;
LOCK(Checkpoints::cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashCheckpoint))
{
// We haven't received the checkpoint chain, keep the checkpoint as pending
Checkpoints::hashPendingCheckpoint = hashCheckpoint;
Checkpoints::checkpointMessagePending = *this;
printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str());
// Ask this guy to fill in what we're missing
if (pfrom)
{
pfrom->PushGetBlocks(pindexBest, hashCheckpoint);
// ask directly as well in case rejected earlier by duplicate
// proof-of-stake because getblocks may not get it this time
pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint));
}
return false;
}
if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint))
return false;
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint];
if (!pindexCheckpoint->IsInMainChain())
{
// checkpoint chain received but not yet main chain
CBlock block;
if (!block.ReadFromDisk(pindexCheckpoint))
return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
if (!block.SetBestChain(txdb, pindexCheckpoint))
{
Checkpoints::hashInvalidCheckpoint = hashCheckpoint;
return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
}
}
if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint))
return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str());
Checkpoints::checkpointMessage = *this;
Checkpoints::hashPendingCheckpoint = 0;
Checkpoints::checkpointMessagePending.SetNull();
printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str());
return true;
}
// Copyright (c) 2009-2012 The Bitcoin developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "checkpoints.h"
#include "txdb.h"
#include "main.h"
#include "uint256.h"
namespace Checkpoints
{
typedef std::map<int, uint256> MapCheckpoints;
//
// What makes a good checkpoint block?
// + Is surrounded by blocks with reasonable timestamps
// (no blocks before with a timestamp after, none after with
// timestamp before)
// + Contains no strange transactions
//
static MapCheckpoints mapCheckpoints = {
{ 0, hashGenesisBlockOfficial },
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
// Recent finality pin (PoS era). Closes the long unchecked span from
// 17650 to the live tip so stale-bootstrap / low-trust forks below
// this height are rejected outright. Hash from the canonical chain.
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
// gap between the last hardcoded checkpoint and the live tip stays bounded.
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
// unverified blocks at tip — a peer feeding fork blocks at those heights
// could trick an IBD node into accepting a divergent chain. With these
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
// All hashes verified against the canonical chain on 2026-07-01.
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
// UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify
// P2P-delivered snapshots without trusting any peer.
//
// Maintainers: after producing a snapshot, sha256 the file and add an entry
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
};
static MapCheckpoints mapCheckpointsTestnet = {
{ 0, hashGenesisBlockTestNet },
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
};
bool CheckHardened(int nHeight, const uint256& hash)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
if (i == checkpoints.end()) return true;
return hash == i->second;
}
bool IsKnownCheckpoint(int nHeight, const uint256& hash)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
if (i == checkpoints.end()) return false;
return hash == i->second;
}
int GetTotalBlocksEstimate()
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
return checkpoints.rbegin()->first;
}
int GetBestSnapshotHeight()
{
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
if (snaps.empty()) return 0;
return snaps.rbegin()->first;
}
bool GetSnapshotHash(int nHeight, uint256& fileHashOut)
{
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
auto it = snaps.find(nHeight);
if (it == snaps.end()) return false;
fileHashOut = it->second;
return true;
}
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex)
{
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it)
{
const uint256& hash = it->second;
std::map<uint256, CBlockIndex*>::const_iterator t = mapBlockIndex.find(hash);
if (t != mapBlockIndex.end())
return t->second;
}
return nullptr;
}
// triangles: synchronized checkpoint (centrally broadcasted)
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
CSyncCheckpoint checkpointMessage;
CSyncCheckpoint checkpointMessagePending;
uint256 hashInvalidCheckpoint = 0;
CCriticalSection cs_hashSyncCheckpoint;
// triangles: get last synchronized checkpoint
CBlockIndex* GetLastSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashSyncCheckpoint))
error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
else
return mapBlockIndex[hashSyncCheckpoint];
return nullptr;
}
// triangles: only descendant of current sync-checkpoint is allowed
bool ValidateSyncCheckpoint(uint256 hashCheckpoint)
{
if (!mapBlockIndex.count(hashSyncCheckpoint))
return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
if (!mapBlockIndex.count(hashCheckpoint))
return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str());
CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint];
CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint];
if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight)
{
// Received an older checkpoint, trace back from current checkpoint
// to the same height of the received checkpoint to verify
// that current checkpoint should be a descendant block
CBlockIndex* pindex = pindexSyncCheckpoint;
while (pindex->nHeight > pindexCheckpointRecv->nHeight)
if (!(pindex = pindex->pprev))
return error("ValidateSyncCheckpoint: pprev null - block index structure failure");
if (pindex->GetBlockHash() != hashCheckpoint)
{
hashInvalidCheckpoint = hashCheckpoint;
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
}
return false; // ignore older checkpoint
}
// Received checkpoint should be a descendant block of the current
// checkpoint. Trace back to the same height of current checkpoint
// to verify.
CBlockIndex* pindex = pindexCheckpointRecv;
while (pindex->nHeight > pindexSyncCheckpoint->nHeight)
if (!(pindex = pindex->pprev))
return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure");
if (pindex->GetBlockHash() != hashSyncCheckpoint)
{
hashInvalidCheckpoint = hashCheckpoint;
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
}
return true;
}
bool WriteSyncCheckpoint(const uint256& hashCheckpoint)
{
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
txdb.TxnBegin();
if (!txdb.WriteSyncCheckpoint(hashCheckpoint))
{
txdb.TxnAbort();
return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
}
if (!txdb.TxnCommit())
return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
Checkpoints::hashSyncCheckpoint = hashCheckpoint;
return true;
}
bool AcceptPendingSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint))
{
if (!ValidateSyncCheckpoint(hashPendingCheckpoint))
{
hashPendingCheckpoint = 0;
checkpointMessagePending.SetNull();
return false;
}
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint];
if (!pindexCheckpoint->IsInMainChain())
{
CBlock block;
if (!block.ReadFromDisk(pindexCheckpoint))
return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
if (!block.SetBestChain(txdb, pindexCheckpoint))
{
hashInvalidCheckpoint = hashPendingCheckpoint;
return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
}
}
if (!WriteSyncCheckpoint(hashPendingCheckpoint))
return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
hashPendingCheckpoint = 0;
checkpointMessage = checkpointMessagePending;
checkpointMessagePending.SetNull();
printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str());
// relay the checkpoint
if (!checkpointMessage.IsNull())
{
for (CNode* pnode : vNodes)
checkpointMessage.RelayTo(pnode);
}
return true;
}
return false;
}
// Automatically select a suitable sync-checkpoint
uint256 AutoSelectSyncCheckpoint()
{
const CBlockIndex *pindex = pindexBest;
// Search backward for a block within max span and maturity window
while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight))
pindex = pindex->pprev;
return pindex->GetBlockHash();
}
// Check against synchronized checkpoint
// Disabled: master key removed in V5, no new sync checkpoints possible.
// Always returns true to prevent stale DB-persisted checkpoints from blocking IBD.
bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev)
{
return true;
}
bool WantedByPendingSyncCheckpoint(uint256 hashBlock)
{
LOCK(cs_hashSyncCheckpoint);
if (hashPendingCheckpoint == 0)
return false;
if (hashBlock == hashPendingCheckpoint)
return true;
if (mapOrphanBlocks.count(hashPendingCheckpoint)
&& hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get()))
return true;
return false;
}
// triangles: reset synchronized checkpoint to last hardened checkpoint
bool ResetSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
const uint256& hash = mapCheckpoints.rbegin()->second;
if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain())
{
// checkpoint block accepted but not yet in main chain
printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str());
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlock block;
if (!block.ReadFromDisk(mapBlockIndex[hash]))
return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str());
if (!block.SetBestChain(txdb, mapBlockIndex[hash]))
{
return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str());
}
}
else if(!mapBlockIndex.count(hash))
{
// checkpoint block not yet accepted
hashPendingCheckpoint = hash;
checkpointMessagePending.SetNull();
printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str());
}
for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it)
{
const uint256& hash = it->second;
if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain())
{
if (!WriteSyncCheckpoint(hash))
return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str());
printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str());
return true;
}
}
return false;
}
void AskForPendingSyncCheckpoint(CNode* pfrom)
{
LOCK(cs_hashSyncCheckpoint);
if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint)))
pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint));
}
bool SetCheckpointPrivKey(std::string strPrivKey)
{
// Test signing a sync-checkpoint with genesis block
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return false;
// Test signing successful, proceed
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
return true;
}
bool SendSyncCheckpoint(uint256 hashCheckpoint)
{
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = hashCheckpoint;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
if (CSyncCheckpoint::strMasterPrivKey.empty())
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
{
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
return false;
}
// Relay checkpoint
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
checkpoint.RelayTo(pnode);
}
return true;
}
// Is the sync-checkpoint outside maturity window?
bool IsMatureSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashSyncCheckpoint))
return true; // no valid sync checkpoint, treat as mature
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity ||
pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime());
}
}
// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork)
const std::string CSyncCheckpoint::strMasterPubKey = "";
std::string CSyncCheckpoint::strMasterPrivKey = "";
// triangles: verify signature of sync-checkpoint message
// Master key system disabled - checkpoint signatures are no longer required
bool CSyncCheckpoint::CheckSignature()
{
// Deserialize the checkpoint data without signature verification
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
sMsg >> *(CUnsignedSyncCheckpoint*)this;
return true;
}
// triangles: process synchronized checkpoint
bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom)
{
if (!CheckSignature())
return false;
LOCK(Checkpoints::cs_hashSyncCheckpoint);
if (!mapBlockIndex.count(hashCheckpoint))
{
// We haven't received the checkpoint chain, keep the checkpoint as pending
Checkpoints::hashPendingCheckpoint = hashCheckpoint;
Checkpoints::checkpointMessagePending = *this;
printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str());
// Ask this guy to fill in what we're missing
if (pfrom)
{
pfrom->PushGetBlocks(pindexBest, hashCheckpoint);
// ask directly as well in case rejected earlier by duplicate
// proof-of-stake because getblocks may not get it this time
pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint));
}
return false;
}
if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint))
return false;
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint];
if (!pindexCheckpoint->IsInMainChain())
{
// checkpoint chain received but not yet main chain
CBlock block;
if (!block.ReadFromDisk(pindexCheckpoint))
return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
if (!block.SetBestChain(txdb, pindexCheckpoint))
{
Checkpoints::hashInvalidCheckpoint = hashCheckpoint;
return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
}
}
if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint))
return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str());
Checkpoints::checkpointMessage = *this;
Checkpoints::hashPendingCheckpoint = 0;
Checkpoints::checkpointMessagePending.SetNull();
printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str());
return true;
}
+18 -18
View File
@@ -1,19 +1,19 @@
#ifndef CLIENTVERSION_H
#define CLIENTVERSION_H
//
// client versioning
//
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 9
#define CLIENT_VERSION_REVISION 22
#ifndef CLIENTVERSION_H
#define CLIENTVERSION_H
//
// client versioning
//
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 6
#define CLIENT_VERSION_MINOR 1
#define CLIENT_VERSION_REVISION 1
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
// Don't merge these into one macro!
#define STRINGIZE(X) DO_STRINGIZE(X)
#define DO_STRINGIZE(X) #X
#endif // CLIENTVERSION_H
// Converts the parameter X to a string after macro replacement on X has been performed.
// Don't merge these into one macro!
#define STRINGIZE(X) DO_STRINGIZE(X)
#define DO_STRINGIZE(X) #X
#endif // CLIENTVERSION_H
+466
View File
@@ -0,0 +1,466 @@
// Copyright (c) 2024 Triangles developers
// I2P (SAM v3) transport support
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "i2p.h"
#include "util.h"
#include "netbase.h"
#include "protocol.h" // CAddress
#include "net.h" // AddI2PInboundNode(), GetListenPort()
#include <openssl/sha.h>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace fs = std::filesystem;
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#ifndef closesocket
#define closesocket close
#endif
#endif
// I2P uses a base64 variant where '+' -> '-' and '/' -> '~'.
static const char* pI2PBase64 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
static std::vector<unsigned char> DecodeI2PBase64(const std::string& str)
{
int table[256];
for (int i = 0; i < 256; i++) table[i] = -1;
for (int i = 0; i < 64; i++) table[(unsigned char)pI2PBase64[i]] = i;
std::vector<unsigned char> out;
int bits = 0; uint32_t buf = 0;
for (char c : str) {
if (c == '=' || c == '\r' || c == '\n') continue;
int v = table[(unsigned char)c];
if (v < 0) continue; // skip anything unexpected
buf = (buf << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((unsigned char)((buf >> bits) & 0xFF));
}
}
return out;
}
CI2PSession* CI2PSession::GetInstance()
{
static CI2PSession instance;
return &instance;
}
CI2PSession::CI2PSession()
: samHost(I2P_DEFAULT_SAM_HOST), samPort(I2P_DEFAULT_SAM_PORT),
hSession(INVALID_SOCKET), fEnabled(false), fActive(false), fShutdown(false)
{
}
CI2PSession::~CI2PSession()
{
Stop();
}
std::string CI2PSession::GetB32Address()
{
std::lock_guard<std::mutex> lock(cs);
return b32Address;
}
// --- low level SAM helpers -------------------------------------------------
bool CI2PSession::SamConnect(SOCKET& hSocketRet)
{
SOCKET hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hSocket == INVALID_SOCKET)
return false;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)samPort);
addr.sin_addr.s_addr = inet_addr(samHost.c_str());
if (connect(hSocket, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
closesocket(hSocket);
return false;
}
hSocketRet = hSocket;
return true;
}
bool CI2PSession::SamSendLine(SOCKET hSocket, const std::string& strLine)
{
std::string out = strLine + "\n";
const char* p = out.c_str();
size_t left = out.size();
while (left > 0) {
int n = send(hSocket, p, (int)left, MSG_NOSIGNAL);
if (n <= 0)
return false;
p += n;
left -= n;
}
return true;
}
bool CI2PSession::SamRecvLine(SOCKET hSocket, std::string& strLineRet)
{
strLineRet.clear();
char c;
// SAM replies are newline terminated; read one byte at a time so we stop
// exactly at the boundary and leave any following stream data untouched.
for (int i = 0; i < 16384; i++) {
int n = recv(hSocket, &c, 1, 0);
if (n <= 0)
return false;
if (c == '\n')
return true;
if (c != '\r')
strLineRet += c;
}
return false;
}
std::string CI2PSession::SamGetValue(const std::string& strReply, const std::string& strKey)
{
// Tokens are space separated KEY=VALUE pairs. VALUE runs to the next space.
std::string needle = strKey + "=";
size_t pos = strReply.find(needle);
if (pos == std::string::npos)
return "";
pos += needle.size();
size_t end = strReply.find(' ', pos);
if (end == std::string::npos)
end = strReply.size();
return strReply.substr(pos, end - pos);
}
bool CI2PSession::SamHandshake(SOCKET hSocket)
{
if (!SamSendLine(hSocket, "HELLO VERSION MIN=3.1 MAX=3.3"))
return false;
std::string reply;
if (!SamRecvLine(hSocket, reply))
return false;
if (SamGetValue(reply, "RESULT") != "OK") {
printf("I2P: SAM handshake failed: %s\n", reply.c_str());
return false;
}
return true;
}
std::string CI2PSession::DestToB32(const std::string& strB64Dest)
{
std::vector<unsigned char> dest = DecodeI2PBase64(strB64Dest);
if (dest.empty())
return "";
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(dest.data(), dest.size(), hash);
std::string b32 = EncodeBase32(hash, SHA256_DIGEST_LENGTH);
// I2P b32 addresses are unpadded.
while (!b32.empty() && b32[b32.size() - 1] == '=')
b32.erase(b32.size() - 1);
return b32 + ".b32.i2p";
}
// --- session bring-up ------------------------------------------------------
bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet)
{
fs::path keyPath = GetDataDir() / "i2p_private_key";
// Reuse an existing persistent destination if we have one.
{
std::ifstream f(keyPath.string().c_str());
if (f.is_open()) {
std::string line;
std::getline(f, line);
while (!line.empty() &&
(line[line.size() - 1] == '\r' || line[line.size() - 1] == '\n'))
line.erase(line.size() - 1);
if (!line.empty()) {
strPrivKeyRet = line;
printf("I2P: loaded persistent destination from %s\n",
keyPath.string().c_str());
return true;
}
}
}
// Generate a fresh destination via the bridge (Ed25519, SIGNATURE_TYPE=7).
SOCKET hSocket = INVALID_SOCKET;
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
return false;
}
bool ok = false;
if (SamSendLine(hSocket, "DEST GENERATE SIGNATURE_TYPE=7")) {
std::string reply;
if (SamRecvLine(hSocket, reply)) {
std::string priv = SamGetValue(reply, "PRIV");
if (!priv.empty()) {
strPrivKeyRet = priv;
std::ofstream out(keyPath.string().c_str(), std::ios::trunc);
if (out.is_open()) {
out << priv << std::endl;
out.close();
printf("I2P: generated and saved new persistent destination\n");
ok = true;
} else {
printf("I2P: WARNING could not write %s\n", keyPath.string().c_str());
ok = true; // still usable for this run
}
}
}
}
closesocket(hSocket);
return ok;
}
bool CI2PSession::CreateSession()
{
if (!SamConnect(hSession))
return false;
if (!SamHandshake(hSession))
return false;
std::ostringstream id;
id << "triangles-" << (uint64_t)GetTime() << "-" << (uint64_t)(GetRand(1000000));
sessionId = id.str();
std::string cmd = "SESSION CREATE STYLE=STREAM ID=" + sessionId +
" DESTINATION=" + privateKey + " SIGNATURE_TYPE=7";
if (!SamSendLine(hSession, cmd))
return false;
std::string reply;
if (!SamRecvLine(hSession, reply))
return false;
if (SamGetValue(reply, "RESULT") != "OK") {
printf("I2P: SESSION CREATE failed: %s\n", reply.c_str());
return false;
}
// The bridge echoes the (possibly newly assigned) private key back.
std::string echoed = SamGetValue(reply, "DESTINATION");
if (!echoed.empty())
privateKey = echoed;
return true;
}
bool CI2PSession::ResolveMyB32()
{
SOCKET hSocket = INVALID_SOCKET;
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
return false;
}
bool ok = false;
if (SamSendLine(hSocket, "NAMING LOOKUP NAME=ME")) {
std::string reply;
if (SamRecvLine(hSocket, reply) && SamGetValue(reply, "RESULT") == "OK") {
std::string dest = SamGetValue(reply, "VALUE");
std::string b32 = DestToB32(dest);
if (!b32.empty()) {
std::lock_guard<std::mutex> lock(cs);
b32Address = b32;
ok = true;
}
}
}
closesocket(hSocket);
return ok;
}
bool CI2PSession::Start()
{
if (!GetBoolArg("-i2p", true)) {
printf("I2P: disabled (-i2p=0)\n");
return false;
}
fEnabled.store(true);
// -i2psam=host:port overrides the default SAM bridge endpoint.
std::string sam = GetArg("-i2psam", "");
if (!sam.empty()) {
int port = I2P_DEFAULT_SAM_PORT;
std::string host;
SplitHostPort(sam, port, host);
if (!host.empty()) samHost = host;
if (port > 0) samPort = port;
}
printf("I2P: connecting to SAM bridge at %s:%d\n", samHost.c_str(), samPort);
if (!LoadOrCreateDestination(privateKey)) {
printf("I2P: ERROR could not obtain a destination. Is an I2P router with "
"the SAM bridge enabled running at %s:%d?\n", samHost.c_str(), samPort);
return false;
}
if (!CreateSession()) {
printf("I2P: ERROR failed to create SAM STREAM session\n");
if (hSession != INVALID_SOCKET) { closesocket(hSession); hSession = INVALID_SOCKET; }
return false;
}
if (!ResolveMyB32())
printf("I2P: WARNING could not resolve our own .b32.i2p address yet\n");
fActive.store(true);
fShutdown.store(false);
printf("I2P: session active. Our address: %s\n", GetB32Address().c_str());
// Register our I2P address as a local address so peers can learn it.
CService meI2P;
if (!b32Address.empty() && meI2P.SetSpecial(b32Address)) {
meI2P.SetPort((unsigned short)GetListenPort());
AddLocal(meI2P, LOCAL_MANUAL);
}
acceptThread = std::thread(&CI2PSession::AcceptLoop, this);
return true;
}
void CI2PSession::Stop()
{
if (!fEnabled.load())
return;
fShutdown.store(true);
fActive.store(false);
if (hSession != INVALID_SOCKET) {
closesocket(hSession);
hSession = INVALID_SOCKET;
}
if (acceptThread.joinable())
acceptThread.join();
fEnabled.store(false);
printf("I2P: session stopped\n");
}
// --- inbound ---------------------------------------------------------------
void CI2PSession::AcceptLoop()
{
while (!fShutdown.load()) {
SOCKET hSocket = INVALID_SOCKET;
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
if (fShutdown.load()) break;
MilliSleep(2000);
continue;
}
// Block here until a peer dials us; the router then streams the remote
// destination on its own line, after which the socket carries data.
if (!SamSendLine(hSocket, "STREAM ACCEPT ID=" + sessionId + " SILENT=false")) {
closesocket(hSocket);
MilliSleep(1000);
continue;
}
std::string status;
if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") {
if (!fShutdown.load())
printf("I2P: STREAM ACCEPT rejected: %s\n", status.c_str());
closesocket(hSocket);
MilliSleep(1000);
continue;
}
std::string remoteDest;
if (!SamRecvLine(hSocket, remoteDest)) {
closesocket(hSocket);
continue;
}
if (fShutdown.load()) {
closesocket(hSocket);
break;
}
// The first token is the remote full destination (base64).
std::string destTok = remoteDest;
size_t sp = destTok.find(' ');
if (sp != std::string::npos)
destTok = destTok.substr(0, sp);
std::string b32 = DestToB32(destTok);
CAddress addr;
if (b32.empty() || !addr.SetSpecial(b32)) {
printf("I2P: could not parse inbound remote destination\n");
closesocket(hSocket);
continue;
}
addr.nServices = 0;
addr.nTime = GetTime();
// Hand the live data socket to the net layer as an inbound peer.
printf("I2P: inbound connection from %s\n", b32.c_str());
AddI2PInboundNode(hSocket, addr);
}
}
// --- outbound --------------------------------------------------------------
bool CI2PSession::Connect(const std::string& strDest, SOCKET& hSocketRet)
{
if (!fActive.load())
return false;
SOCKET hSocket = INVALID_SOCKET;
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
return false;
}
if (!SamSendLine(hSocket, "STREAM CONNECT ID=" + sessionId +
" DESTINATION=" + strDest + " SILENT=false")) {
closesocket(hSocket);
return false;
}
std::string status;
if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") {
printf("I2P: STREAM CONNECT to %s failed: %s\n", strDest.c_str(), status.c_str());
closesocket(hSocket);
return false;
}
// Socket is now a bidirectional stream to the peer.
hSocketRet = hSocket;
return true;
}
bool StartI2P()
{
return CI2PSession::GetInstance()->Start();
}
void StopI2P()
{
CI2PSession::GetInstance()->Stop();
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) 2024 Triangles developers
// I2P (SAM v3) transport support
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// This module gives Triangles real I2P connectivity that mirrors the existing
// embedded-Tor design: instead of a SOCKS proxy it talks the SAM v3 protocol
// to a locally running I2P router (i2pd or Java I2P) and obtains a persistent
// I2P destination whose ".b32.i2p" address is shown alongside the .onion
// address. The wallet:
// * creates / loads a persistent destination (i2p_private_key in datadir),
// * runs a STREAM session so peers can dial us,
// * accepts inbound I2P streams and feeds them to the net layer,
// * dials outbound ".b32.i2p" peers through the same session.
//
// A running I2P router with its SAM bridge enabled (default 127.0.0.1:7656) is
// required; nothing is bundled. Enable with -i2p and optionally -i2psam=host:port.
#ifndef TRIANGLES_I2P_H
#define TRIANGLES_I2P_H
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include "compat.h" // SOCKET / INVALID_SOCKET
// Default SAM bridge endpoint exposed by i2pd / Java I2P.
#define I2P_DEFAULT_SAM_HOST "127.0.0.1"
#define I2P_DEFAULT_SAM_PORT 7656
// Manages a single persistent I2P STREAM session over SAM v3.
class CI2PSession
{
public:
static CI2PSession* GetInstance();
// Bring the session up: connect to the SAM bridge, load/generate the
// persistent destination and start accepting inbound streams.
// Returns false (and logs) if no router/SAM bridge is reachable.
bool Start();
// Tear the session down and stop the accept loop.
void Stop();
bool IsEnabled() const { return fEnabled.load(); }
bool IsActive() const { return fActive.load(); }
// Our own ".b32.i2p" address (empty until the session is up).
std::string GetB32Address();
// Dial a remote ".b32.i2p" (or full base64 destination) through the
// session. On success hSocketRet is a connected, blocking data socket the
// caller can hand to a CNode. The caller takes ownership of the socket.
bool Connect(const std::string& strDest, SOCKET& hSocketRet);
private:
CI2PSession();
~CI2PSession();
// --- low level SAM helpers ---
bool SamConnect(SOCKET& hSocketRet); // raw TCP to the bridge
bool SamHandshake(SOCKET hSocket); // HELLO VERSION
bool SamSendLine(SOCKET hSocket, const std::string& strLine);
bool SamRecvLine(SOCKET hSocket, std::string& strLineRet);
static std::string SamGetValue(const std::string& strReply, const std::string& strKey);
bool LoadOrCreateDestination(std::string& strPrivKeyRet);
bool CreateSession(); // SESSION CREATE
bool ResolveMyB32(); // NAMING LOOKUP ME
void AcceptLoop(); // inbound STREAM ACCEPT
// Compute the ".b32.i2p" address from a base64 (I2P alphabet) destination.
static std::string DestToB32(const std::string& strB64Dest);
std::string samHost;
int samPort;
std::string sessionId;
std::string privateKey; // persistent destination private key (base64)
std::string b32Address; // our own .b32.i2p
SOCKET hSession; // long-lived control socket owning the session
std::atomic<bool> fEnabled;
std::atomic<bool> fActive;
std::atomic<bool> fShutdown;
std::thread acceptThread;
std::mutex cs;
};
// Convenience: start/stop from init.cpp.
bool StartI2P();
void StopI2P();
#endif // TRIANGLES_I2P_H
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
I2PD_SRC_DIR="${I2PD_SRC_DIR:-$ROOT_DIR/i2pd-src}"
if [[ ! -d "$I2PD_SRC_DIR" ]]; then
echo "i2pd source tree not found at: $I2PD_SRC_DIR" >&2
exit 1
fi
cd "$I2PD_SRC_DIR"
echo "Building libi2pd static libraries from: $I2PD_SRC_DIR"
NPROC_VAL="${NPROC:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}"
# Detect the correct OpenSSL formula path on macOS. The i2pd
# Makefile.homebrew hardcodes openssl@3.5 but Homebrew may install
# openssl@3 instead. Command-line make variables override Makefile
# assignments, so passing SSLROOT=<detected> fixes the include path.
EXTRA_MAKE_ARGS=()
if [[ "$(uname -s)" == "Darwin" ]]; then
if [[ -d "/opt/homebrew/opt/openssl@3" ]]; then
SSLROOT="/opt/homebrew/opt/openssl@3"
elif [[ -d "/usr/local/opt/openssl@3" ]]; then
SSLROOT="/usr/local/opt/openssl@3"
fi
if [[ -n "${SSLROOT:-}" ]]; then
echo "Detected OpenSSL at: $SSLROOT (overriding Makefile.homebrew)"
EXTRA_MAKE_ARGS+=("SSLROOT=${SSLROOT}")
fi
fi
# i2pd uses a hand-written Makefile system. We build only the static library
# targets (libi2pd.a, libi2pdclient.a, libi2pdlang.a), NOT the standalone
# i2pd daemon binary, which pulls in HTTPServer/I2PControl deps we don't need
# and can OOM on memory-constrained build machines.
make -j"$NPROC_VAL" USE_STATIC=no "${EXTRA_MAKE_ARGS[@]}" libi2pd.a libi2pdclient.a libi2pdlang.a
echo
echo "Build finished. Static libraries:"
ls -lh libi2pd*.a
echo
echo "Suggested next step for Triangles:"
echo " cmake -DUSE_I2P_EMBEDDED=ON -DI2P_SOURCE_ROOT=src/i2p/i2pd-src .."
+700
View File
@@ -0,0 +1,700 @@
// Copyright (c) 2025-2026 Triangles developers
// Embedded I2P (i2pd) integration - runs an I2P router in-process
// Distributed under the MIT/X11 software license
//
// BUILD REQUIREMENT: Link against libi2pd.a + libi2pd_client.a built from
// the PurpleI2P/i2pd source tree (src/i2p/i2pd-src).
//
// This file compiles in two modes:
// 1. ENABLE_I2P_EMBEDDED defined: full embedded i2pd via i2p::api
// 2. ENABLE_I2P_EMBEDDED not defined: stubs that report I2P unavailable
#include "i2p_embedded.h"
#include "../util.h"
#include "../net.h"
#include <filesystem>
#include <thread>
#include <fstream>
#include <cstring>
#include <chrono>
#include <ctime>
#include <vector>
#include <string>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
// ===========================================================================
// CI2PSamSocket — SAM v3 direct streaming implementation
// ===========================================================================
//
// Protocol reference: https://geti2p.net/en/docs/api/samv3
//
// The SAM bridge is a simple line-oriented text protocol over TCP. After
// HELLO + SESSION CREATE + STREAM CONNECT succeed, the socket becomes a
// raw bidirectional byte stream to the I2P destination — no further SAM
// framing is needed and there is zero SOCKS overhead.
static std::atomic<unsigned int> g_samSessionSeq{0};
CI2PSamSocket::CI2PSamSocket()
: rawSocket(I2P_INVALID_SOCKET)
{
}
CI2PSamSocket::~CI2PSamSocket()
{
CloseSocket();
}
void CI2PSamSocket::CloseSocket()
{
if (rawSocket != I2P_INVALID_SOCKET) {
#ifdef WIN32
closesocket(rawSocket);
#else
close(rawSocket);
#endif
rawSocket = I2P_INVALID_SOCKET;
}
}
I2pSocket_t CI2PSamSocket::GetRawSocket()
{
I2pSocket_t fd = rawSocket;
rawSocket = I2P_INVALID_SOCKET; // transfer ownership
return fd;
}
bool CI2PSamSocket::SamConnect(const std::string& host, int port)
{
CloseSocket();
#ifdef WIN32
rawSocket = (I2pSocket_t)::socket(AF_INET, SOCK_STREAM, 0);
if (rawSocket == INVALID_SOCKET) {
#else
rawSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (rawSocket < 0) {
#endif
lastError = "SAM: failed to create socket";
return false;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // SAM is always local
addr.sin_port = htons((uint16_t)port);
if (::connect(rawSocket, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
lastError = "SAM: cannot connect to bridge at 127.0.0.1:" + std::to_string(port);
CloseSocket();
return false;
}
return true;
}
bool CI2PSamSocket::SendLine(const std::string& line)
{
std::string msg = line + "\n";
const char* data = msg.data();
size_t remaining = msg.size();
while (remaining > 0) {
#ifdef WIN32
int n = ::send(rawSocket, data, (int)remaining, 0);
#else
ssize_t n = ::send(rawSocket, data, remaining, MSG_NOSIGNAL);
#endif
if (n <= 0) {
lastError = "SAM: send failed";
return false;
}
data += n;
remaining -= (size_t)n;
}
return true;
}
bool CI2PSamSocket::ReadLine(std::string& lineOut)
{
// Look for a complete line (terminated by \n) in recvBuffer first.
for (;;) {
size_t nl = recvBuffer.find('\n');
if (nl != std::string::npos) {
lineOut = recvBuffer.substr(0, nl);
// Strip trailing \r (SAM bridge always uses \n, but be tolerant)
if (!lineOut.empty() && lineOut.back() == '\r')
lineOut.pop_back();
recvBuffer.erase(0, nl + 1);
return true;
}
char buf[4096];
#ifdef WIN32
int n = ::recv(rawSocket, buf, sizeof(buf), 0);
#else
ssize_t n = ::recv(rawSocket, buf, sizeof(buf), 0);
#endif
if (n <= 0) {
lastError = "SAM: connection closed while waiting for reply";
return false;
}
recvBuffer.append(buf, (size_t)n);
}
}
std::string CI2PSamSocket::ParseValue(const std::string& line, const std::string& key)
{
// Find KEY=VALUE token within a space-separated SAM response line.
std::string needle = key + "=";
size_t pos = line.find(needle);
if (pos == std::string::npos)
return {};
pos += needle.size();
size_t end = line.find(' ', pos);
if (end == std::string::npos)
return line.substr(pos);
return line.substr(pos, end - pos);
}
bool CI2PSamSocket::Connect(const std::string& dest_b32, int port,
const std::string& samHost, int samPort)
{
CloseSocket();
lastError.clear();
recvBuffer.clear();
if (dest_b32.empty()) {
lastError = "SAM: empty destination";
return false;
}
// Generate a unique session ID for this connection.
unsigned int seq = ++g_samSessionSeq;
sessionId = "triangles-" + std::to_string(seq) + "-" +
std::to_string((unsigned long)std::time(nullptr));
// ----------------------------------------------------------------
// Step 0: TCP connect to the SAM bridge
// ----------------------------------------------------------------
if (!SamConnect(samHost, samPort)) {
// lastError already set by SamConnect
return false;
}
// ----------------------------------------------------------------
// Step 1: HELLO handshake
// C → S: HELLO VERSION MIN=3.1 MAX=3.1
// S → C: HELLO REPLY RESULT=OK VERSION=3.1
// ----------------------------------------------------------------
if (!SendLine("HELLO VERSION MIN=3.1 MAX=3.1")) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM HELLO failed: " + reply;
CloseSocket();
return false;
}
}
// ----------------------------------------------------------------
// Step 2: SESSION CREATE (transient destination)
// C → S: SESSION CREATE STYLE=STREAM ID=<id> DESTINATION=TRANSIENT
// S → C: SESSION STATUS RESULT=OK DESTINATION=<base64>
// ----------------------------------------------------------------
if (!SendLine("SESSION CREATE STYLE=STREAM ID=" + sessionId +
" DESTINATION=TRANSIENT")) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM SESSION CREATE failed: " + reply;
CloseSocket();
return false;
}
// Save the transient local destination (base64) for diagnostics.
localDestination = ParseValue(reply, "DESTINATION");
}
// ----------------------------------------------------------------
// Step 3: STREAM CONNECT to the remote destination
// C → S: STREAM CONNECT ID=<id> DESTINATION=<b32>.i2p
// S → C: STREAM STATUS RESULT=OK
//
// After RESULT=OK the socket is a raw byte stream — no more SAM
// framing is needed.
// ----------------------------------------------------------------
// Ensure destination has the .b32.i2p suffix (accept bare b32 hash too)
std::string dest = dest_b32;
if (dest.find(".i2p") == std::string::npos && dest.find(".b32") == std::string::npos) {
// Looks like a bare b32 hash — append the standard suffix
dest += ".b32.i2p";
}
if (!SendLine("STREAM CONNECT ID=" + sessionId + " DESTINATION=" + dest)) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM STREAM CONNECT to " + dest + " failed: " + reply;
CloseSocket();
return false;
}
}
// Socket is now a raw I2P stream. Any residual bytes in recvBuffer
// belong to the application layer — leave them for the caller.
return true;
}
// ===========================================================================
// CI2PEmbedded — singleton router management
// ===========================================================================
// Singleton
CI2PEmbedded* CI2PEmbedded::instance = nullptr;
CI2PEmbedded* CI2PEmbedded::GetInstance()
{
if (!instance)
instance = new CI2PEmbedded();
return instance;
}
CI2PEmbedded::CI2PEmbedded()
: running(false)
, socksPort(19100)
, samPort(7656)
, serverPort(0)
{
}
CI2PEmbedded::~CI2PEmbedded()
{
Stop();
}
std::string CI2PEmbedded::GetSocksProxy() const
{
return "127.0.0.1:" + std::to_string(socksPort);
}
// ---------------------------------------------------------------------------
// IsSamAvailable — quick TCP probe of the SAM bridge port
// ---------------------------------------------------------------------------
bool CI2PEmbedded::IsSamAvailable() const
{
#ifdef WIN32
SOCKET sock = ::socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
return false;
#else
int sock = ::socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
return false;
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons((uint16_t)samPort);
bool ok = (::connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
return ok;
}
// ---------------------------------------------------------------------------
// CreateConnection — factory for SAM v3 direct streaming connections
// ---------------------------------------------------------------------------
CI2PSamSocket* CI2PEmbedded::CreateConnection(const std::string& dest_b32, int port)
{
if (!running.load()) {
return nullptr;
}
auto* sam = new CI2PSamSocket();
if (!sam->Connect(dest_b32, port, "127.0.0.1", samPort)) {
// Caller can inspect via the object — but they don't have it yet,
// so log the error and clean up.
printf("I2P SAM connect failed: %s\n", sam->GetLastError().c_str());
delete sam;
return nullptr;
}
printf("I2P SAM stream connected to %s (raw socket, no SOCKS overhead)\n",
dest_b32.c_str());
return sam;
}
#ifdef ENABLE_I2P_EMBEDDED
// ========================================================================
// Embedded mode: i2pd runs in-process via libi2pd / i2p::api
// ========================================================================
#ifdef WIN32
// MinGW's rpcndr.h (pulled in by winsock2.h/windows.h) #defines
// 'interface' as 'struct' for COM support. i2pd's I2CP.h uses it as a
// parameter name (I2CPServer(const std::string& interface, ...)),
// causing a parse error. Undef before including any i2pd headers.
#undef interface
#endif
// i2pd C++ API
#include "Config.h"
#include "Log.h"
#include "FS.h"
#include "Crypto.h"
#include "NetDb.hpp"
#include "Transports.h"
#include "Tunnel.h"
#include "RouterContext.h"
#include "Streaming.h"
#include "Destination.h"
#include "ClientContext.h"
#include "I2PTunnel.h"
#include "api.h"
static std::unique_ptr<i2p::client::I2PServerTunnel> g_i2pServerTunnel;
static std::shared_ptr<i2p::client::ClientDestination> g_i2pServerDestination;
bool CI2PEmbedded::Start(int socks, int sam, int server)
{
if (running.load()) return true;
lastError.clear();
socksPort = socks;
samPort = sam;
serverPort = server;
i2pHostname.clear();
// Prepare i2pd data directory under the wallet's data dir
i2pDataDir = (::GetDataDir() / "i2p_data").string();
fs::create_directories(i2pDataDir);
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
printf("Embedded I2P: starting i2pd router...\n");
// Write an i2pd.conf configuration file that enables SAM + SOCKS proxy.
// i2pd's config system reads from a file; programmatic option setting is
// fragile across i2pd versions. Writing a minimal conf is robust.
{
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
std::ofstream conf(confPath.string());
if (!conf.is_open()) {
lastError = "Failed to write i2pd.conf";
return false;
}
conf << "# Auto-generated by Triangles embedded I2P\n";
conf << "datadir = " << i2pDataDir << "\n";
conf << "loglevel = info\n";
conf << "\n";
// SOCKS proxy for outbound .i2p connections (P2P transport)
conf << "[socksproxy]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << socksPort << "\n";
conf << "keys = socks-proxy.dat\n";
conf << "\n";
// SAM bridge for SAM v3 direct streaming API
conf << "[sam]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << samPort << "\n";
conf << "\n";
// Disable HTTP webconsole (not needed for embedded use)
conf << "[http]\n";
conf << "enabled = false\n";
conf << "\n";
// Disable I2P control protocol
conf << "[i2pcontrol]\n";
conf << "enabled = false\n";
conf << "\n";
// Disable BOB
conf << "[bob]\n";
conf << "enabled = false\n";
conf << "\n";
conf.close();
}
// Write tunnels.conf BEFORE Start() — ClientContext::Start() reads this
// file to create server/client tunnels. The server tunnel is the I2P
// equivalent of a Tor hidden service: it forwards inbound I2P connections
// to the Triangles P2P listen port.
if (serverPort > 0) {
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
std::ofstream tunnelConf(tunnelConfPath.string());
if (tunnelConf.is_open()) {
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
tunnelConf << "[triangles-p2p]\n";
tunnelConf << "type = server\n";
tunnelConf << "host = 127.0.0.1\n";
tunnelConf << "port = " << serverPort << "\n";
tunnelConf << "keys = triangles-p2p-keys.dat\n";
tunnelConf << "inbound.length = 3\n";
tunnelConf << "outbound.length = 3\n";
tunnelConf << "inbound.quantity = 5\n";
tunnelConf << "outbound.quantity = 5\n";
tunnelConf.close();
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
}
}
// Build argv for i2pd initialization. Pass --datadir and --conf on the
// command line (not just in the conf file) because i2pd's ParseCmdline
// runs BEFORE ParseConfig, and DetectDataDir needs the datadir early.
std::vector<std::string> argvStrings;
argvStrings.push_back("i2pd");
argvStrings.push_back("--datadir");
argvStrings.push_back(i2pDataDir);
argvStrings.push_back("--conf");
argvStrings.push_back((fs::path(i2pDataDir) / "i2pd.conf").string());
std::vector<char*> argvPtrs;
for (auto& s : argvStrings)
argvPtrs.push_back(&s[0]);
argvPtrs.push_back(nullptr);
try {
// ----------------------------------------------------------------
// Phase 1 (synchronous, < 1s): config parse, crypto, router context
// ----------------------------------------------------------------
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
fflush(stdout);
// Mark running immediately so Qt UI shows I2P as active.
running.store(true);
// ----------------------------------------------------------------
// Phase 2 (background thread): StartI2P + client context + bootstrap
//
// i2p::api::StartI2P() → NetDb::Start() → Reseed() can block for
// up to 180s on first run (empty netDb → HTTPS download from public
// I2P reseed servers). Running this on the main init thread freezes
// the GUI splash screen ("Starting embedded I2P router...").
//
// The background thread handles:
// 1. StartI2P (router, netdb, transports, tunnels, reseed)
// 2. client::context.Start (SAM bridge, SOCKS proxy, server tunnel)
// 3. Polling for SOCKS/SAM port readiness (up to 300s)
// 4. .b32.i2p address population
//
// Meanwhile, the main init proceeds immediately. Tor-only mode
// works in the meantime; I2P connectivity comes up asynchronously.
// ----------------------------------------------------------------
printf("Embedded I2P: launching router in background thread...\n");
fflush(stdout);
std::thread([this]() {
try {
// Start the I2P router (netdb, transports, tunnels, reseed)
auto logStream = std::make_shared<std::ostream>(std::cout.rdbuf());
i2p::api::StartI2P(logStream);
fflush(stdout);
printf("Embedded I2P: router started, starting client services...\n");
fflush(stdout);
// Start SAM bridge, SOCKS proxy, and server tunnel
i2p::client::context.Start();
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
socksPort, samPort);
fflush(stdout);
// Wait for SOCKS proxy + SAM bridge to become available
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
bool socksReady = false;
bool samReady = false;
for (int i = 0; i < 300; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return;
}
if (!socksReady) {
#ifdef WIN32
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock != INVALID_SOCKET) {
#else
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock >= 0) {
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons(socksPort);
bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
if (up) {
socksReady = true;
printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n",
socksPort, i + 1);
}
}
}
if (!samReady) {
samReady = IsSamAvailable();
if (samReady) {
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
samPort, i + 1);
}
}
if (socksReady && samReady) {
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
socksPort, samPort);
break;
}
if (i > 0 && i % 30 == 0) {
printf("Embedded I2P: still bootstrapping (%ds elapsed, SOCKS:%s SAM:%s)...\n",
i, socksReady ? "ready" : "wait",
samReady ? "ready" : "wait");
}
}
// Populate .b32.i2p address
try {
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
i2pHostname = identHash.ToBase32() + ".b32.i2p";
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
} catch (...) {
printf("Embedded I2P: .b32.i2p address not yet available, Qt timer will retry\n");
}
fflush(stdout);
} catch (const std::exception& e) {
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
fflush(stdout);
}
}).detach();
printf("Embedded I2P: router init delegated to background thread\n");
fflush(stdout);
return true;
} catch (const std::exception& e) {
lastError = std::string("i2pd initialization failed: ") + e.what();
printf("ERROR: Embedded I2P startup failed: %s\n", e.what());
running.store(false);
return false;
}
}
void CI2PEmbedded::Stop()
{
if (!running.load()) return;
printf("Requesting embedded I2P shutdown...\n");
try {
// Stop client context (SAM, SOCKS, tunnels)
i2p::client::context.Stop();
// Stop the router
i2p::api::StopI2P();
// Terminate crypto
i2p::api::TerminateI2P();
} catch (const std::exception& e) {
printf("WARNING: error during I2P shutdown: %s\n", e.what());
}
running.store(false);
}
#else // !ENABLE_I2P_EMBEDDED
// ========================================================================
// Fallback stubs: embedded I2P not compiled in
// ========================================================================
bool CI2PEmbedded::Start(int socks, int sam, int server)
{
printf("Embedded I2P not compiled in (ENABLE_I2P_EMBEDDED not defined).\n");
socksPort = socks;
samPort = sam;
serverPort = server;
i2pDataDir = (::GetDataDir() / "i2p_data").string();
lastError = "I2P support not compiled in. Build with -DUSE_I2P_EMBEDDED=ON";
return false;
}
void CI2PEmbedded::Stop()
{
running.store(false);
}
#endif // ENABLE_I2P_EMBEDDED
// ========================================================================
// Global hooks (called from init.cpp)
// ========================================================================
bool StartEmbeddedI2P()
{
bool enableI2P = GetBoolArg("-i2p", true);
if (!enableI2P) {
printf("I2P disabled by -i2p=0 flag\n");
return false;
}
int socksPort = GetArg("-i2psocks", 19100);
int samPort = GetArg("-i2psam", 7656);
int serverPort = GetArg("-i2phsport", GetListenPort());
return CI2PEmbedded::GetInstance()->Start(socksPort, samPort, serverPort);
}
void StopEmbeddedI2P()
{
CI2PEmbedded::GetInstance()->Stop();
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) 2025-2026 Triangles developers
// Embedded I2P (i2pd) integration - runs an I2P router in-process
// Distributed under the MIT/X11 software license
#ifndef TRIANGLES_I2P_EMBEDDED_H
#define TRIANGLES_I2P_EMBEDDED_H
#include <string>
#include <atomic>
// Cross-platform socket handle for SAM v3 streaming API.
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
#ifdef WIN32
# include <winsock2.h>
typedef SOCKET I2pSocket_t;
# define I2P_INVALID_SOCKET INVALID_SOCKET
#else
typedef int I2pSocket_t;
# define I2P_INVALID_SOCKET (-1)
#endif
// ---------------------------------------------------------------------------
// CI2PSamSocket — SAM v3 direct streaming socket
//
// Wraps a raw TCP socket to the i2pd SAM bridge. After Connect() succeeds,
// the underlying socket is a bidirectional byte stream to the I2P
// destination with NO SOCKS overhead. The Triangles P2P layer can read and
// write directly once ownership is taken via GetRawSocket().
//
// Lifecycle:
// 1. Construct
// 2. Connect(dest_b32, port) — performs SAM SESSION CREATE + STREAM CONNECT
// 3. GetRawSocket() — take the fd for direct read/write
// 4. The fd must be closed by the caller (e.g. via CloseSocket())
//
// If Connect() fails, GetLastError() returns a human-readable diagnostic.
// ---------------------------------------------------------------------------
class CI2PSamSocket
{
public:
CI2PSamSocket();
~CI2PSamSocket();
CI2PSamSocket(const CI2PSamSocket&) = delete;
CI2PSamSocket& operator=(const CI2PSamSocket&) = delete;
// Perform the full SAM v3 handshake (HELLO → SESSION CREATE → STREAM CONNECT)
// to reach dest_b32 (a .b32.i2p hostname). samHost/samPort identify the
// local SAM bridge (default 127.0.0.1:7656).
//
// The |port| argument is accepted for API symmetry with the Tor SOCKS
// connection factory but is not part of the SAM v3 STREAM CONNECT request
// (I2P destinations are address-only; there is no TCP-style port).
bool Connect(const std::string& dest_b32, int port,
const std::string& samHost = "127.0.0.1", int samPort = 7656);
// Release ownership of the raw socket fd. After this call the object
// will not close it and the caller is responsible for cleanup.
// Returns I2P_INVALID_SOCKET if not connected.
I2pSocket_t GetRawSocket();
// Close the socket if still owned (no-op after GetRawSocket()).
void CloseSocket();
bool IsValid() const { return rawSocket != I2P_INVALID_SOCKET; }
std::string GetLastError() const { return lastError; }
// The base64 local destination returned by SESSION STATUS (may be empty).
const std::string& GetLocalDestination() const { return localDestination; }
private:
I2pSocket_t rawSocket;
std::string sessionId;
std::string localDestination;
std::string lastError;
std::string recvBuffer; // partial SAM response buffering
// --- SAM protocol helpers ---
bool SamConnect(const std::string& host, int port);
bool SendLine(const std::string& line);
bool ReadLine(std::string& lineOut);
static std::string ParseValue(const std::string& line, const std::string& key);
};
// Embedded I2P router state
class CI2PEmbedded
{
private:
static CI2PEmbedded* instance;
std::atomic<bool> running;
int socksPort; // i2pd SOCKS proxy port (for outbound .i2p connections)
int samPort; // i2pd SAM bridge port (for SAM v3 protocol)
int serverPort; // Triangles P2P listen port (for incoming I2P connections)
std::string i2pDataDir; // i2pd data directory (under wallet datadir)
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
std::string lastError;
public:
static CI2PEmbedded* GetInstance();
CI2PEmbedded();
~CI2PEmbedded();
// Start embedded i2pd router (blocks calling thread briefly during init)
bool Start(int socksPort = 19100, int samPort = 7656, int serverPort = 0);
// Request i2pd to shut down
void Stop();
// Check if i2pd is running
bool IsRunning() const { return running.load(); }
void SetRunning(bool value) { running.store(value); }
// Get the SOCKS5 proxy address for outbound .i2p connections
std::string GetSocksProxy() const;
int GetSocksPort() const { return socksPort; }
int GetSamPort() const { return samPort; }
int GetServerPort() const { return serverPort; }
const std::string& GetDataDir() const { return i2pDataDir; }
// Get our .b32.i2p destination address
std::string GetI2PAddress() const { return i2pHostname; }
std::string GetStartupError() const { return lastError; }
void SetStartupError(const std::string& value) { lastError = value; }
// -------------------------------------------------------------------
// SAM v3 direct streaming API
// -------------------------------------------------------------------
// Create a SAM v3 connection to a .b32.i2p destination.
// Returns a heap-allocated CI2PSamSocket on success (caller owns it
// and must CloseSocket / delete), or nullptr on failure. Use
// GetLastError() on the returned object for diagnostics.
CI2PSamSocket* CreateConnection(const std::string& dest_b32, int port);
// Probe whether the SAM bridge port is accepting TCP connections.
bool IsSamAvailable() const;
};
// Global init/shutdown hooks (called from init.cpp)
bool StartEmbeddedI2P();
void StopEmbeddedI2P();
#endif // TRIANGLES_I2P_EMBEDDED_H
+1
Submodule src/i2p/i2pd-src added at 8497a429dc
+31
View File
@@ -0,0 +1,31 @@
#ifndef TRIANGLES_I2PSEED_H
#define TRIANGLES_I2PSEED_H
// Hardcoded I2P seed nodes for initial peer discovery.
// These are .b32.i2p addresses (Destination hashes).
// Nodes must run i2pd with a server tunnel forwarding to the Triangles P2P port.
//
// NOTE: .b32.i2p addresses are derived from the destination's public key.
// They are generated when the node first creates its I2P tunnel keys.
// Replace these placeholders with actual seed node addresses once deployed.
//
// Dynamic seeds will also be available at:
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
static const char *strMainNetI2PSeed[][1] = {
// SAMI-PC - authoritative wallet node (main PC)
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
// DNS2 - primary bootstrap server (194.233.88.206)
// Generated by embedded i2pd on first run, keys persist in i2p_data/
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
// DNS3 - canonical chain reference (74.208.167.19)
{"hvvr2yys3nll4l6fdywecvn3baw6h5i7bsa2ldbz2e5xwangnn7q.b32.i2p"},
// Hetzner Helsinki - ARM64 staking node (46.62.249.20)
{"2hyeunnkax5du4snip4gdsdicxtmlnagtlkatv57rjpx2kvfssma.b32.i2p"},
{nullptr}
};
static const char *strTestNetI2PSeed[][1] = {
{nullptr}
};
#endif
+368
View File
@@ -0,0 +1,368 @@
// Copyright (c) 2024 Triangles developers
// I2P Router Process Manager - launches and manages a bundled i2pd binary
// Distributed under the MIT/X11 software license
#ifdef WIN32
#define NOMINMAX
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#endif
#include "i2p_process.h"
#include "util.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <vector>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <tlhelp32.h>
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
static CI2PProcess* i2pProcessInstance = nullptr;
CI2PProcess* CI2PProcess::GetInstance()
{
if (!i2pProcessInstance)
i2pProcessInstance = new CI2PProcess();
return i2pProcessInstance;
}
CI2PProcess::CI2PProcess()
: samPort(7656)
, running(false)
, fExternal(false)
#ifdef WIN32
, hProcess(nullptr)
, hJob(nullptr)
, processId(0)
#else
, processId(0)
#endif
{
}
CI2PProcess::~CI2PProcess()
{
Stop();
}
// Try a quick TCP connect; success means something is already listening
// (e.g. the SAM bridge is up, or an external router is running).
bool CI2PProcess::CanConnect(const std::string& host, int port)
{
#ifdef WIN32
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) return false;
#else
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s < 0) return false;
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)port);
addr.sin_addr.s_addr = inet_addr(host.c_str());
bool ok = (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(s);
#else
close(s);
#endif
return ok;
}
std::string CI2PProcess::FindI2pdBinary()
{
std::vector<std::string> candidates;
#ifdef WIN32
const char* exeName = "i2pd.exe";
#else
const char* exeName = "i2pd";
#endif
// 1. Next to the wallet executable (this is how tor.exe is shipped).
try {
fs::path exeDir;
#ifdef WIN32
char buf[MAX_PATH];
if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0)
exeDir = fs::path(buf).parent_path();
#else
exeDir = fs::current_path();
#endif
if (!exeDir.empty()) {
candidates.push_back((exeDir / exeName).string());
candidates.push_back((exeDir / "i2pd" / exeName).string());
candidates.push_back((exeDir / "I2P" / exeName).string());
}
} catch (...) {}
// 2. In / next to the data directory.
candidates.push_back((GetDataDir() / exeName).string());
candidates.push_back((GetDataDir() / "i2pd" / exeName).string());
// 3. Common system locations.
#ifdef WIN32
if (const char* pf = getenv("ProgramFiles"))
candidates.push_back(std::string(pf) + "\\i2pd\\" + exeName);
if (const char* pfx = getenv("ProgramFiles(x86)"))
candidates.push_back(std::string(pfx) + "\\i2pd\\" + exeName);
candidates.push_back(std::string("C:\\i2pd\\") + exeName);
#else
candidates.push_back("/usr/bin/i2pd");
candidates.push_back("/usr/local/bin/i2pd");
candidates.push_back("/opt/i2pd/bin/i2pd");
candidates.push_back("/opt/homebrew/bin/i2pd");
candidates.push_back("/usr/local/opt/i2pd/bin/i2pd");
#endif
for (const std::string& c : candidates) {
try {
if (fs::exists(c) && fs::is_regular_file(c)) {
printf("I2P: found i2pd binary at %s\n", c.c_str());
return c;
}
} catch (...) {}
}
return "";
}
bool CI2PProcess::WriteConfig()
{
fs::path dir(dataDir);
try {
fs::create_directories(dir);
} catch (const std::exception& e) {
lastError = std::string("Cannot create i2pd data directory: ") + e.what();
return false;
}
confPath = (dir / "i2pd.conf").string();
fs::path logPath = dir / "i2pd.log";
std::ofstream conf(confPath.c_str(), std::ios::trunc);
if (!conf.is_open()) {
lastError = "Cannot write i2pd.conf to " + confPath;
return false;
}
conf << "# Triangles Wallet I2P configuration (auto-generated)\n";
conf << "# Do not edit - this file is overwritten on startup\n\n";
conf << "daemon = false\n";
conf << "log = file\n";
conf << "logfile = " << logPath.string() << "\n";
conf << "datadir = " << dir.string() << "\n\n";
// The bridge our SAM client talks to.
conf << "[sam]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << samPort << "\n\n";
// We only need SAM; keep everything else off to minimise footprint.
conf << "[httpproxy]\nenabled = false\n\n";
conf << "[socksproxy]\nenabled = false\n\n";
conf << "[http]\nenabled = false\n\n";
conf << "[i2pcontrol]\nenabled = false\n";
conf.close();
printf("I2P: wrote i2pd config to %s (SAM port %d)\n", confPath.c_str(), samPort);
return true;
}
bool CI2PProcess::Start(const std::string& dataDirIn, int samPortIn)
{
dataDir = dataDirIn;
samPort = samPortIn;
fExternal = false;
lastError.clear();
// If a SAM bridge is already up, use it instead of launching our own.
if (CanConnect("127.0.0.1", samPort)) {
printf("I2P: detected an I2P router already listening on SAM port %d; using it\n", samPort);
fExternal = true;
return true;
}
binaryPath = FindI2pdBinary();
if (binaryPath.empty()) {
lastError = "No i2pd binary found (ship i2pd alongside the wallet, like tor)";
printf("I2P: %s\n", lastError.c_str());
return false;
}
if (!WriteConfig())
return false;
printf("I2P: starting i2pd: %s --conf %s\n", binaryPath.c_str(), confPath.c_str());
#ifdef WIN32
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
ZeroMemory(&pi, sizeof(pi));
std::string cmdLine = "\"" + binaryPath + "\" --conf \"" + confPath + "\"";
if (!CreateProcessA(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr,
FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
DWORD err = ::GetLastError();
lastError = strprintf("CreateProcess failed for i2pd '%s' (Windows error %lu)", binaryPath.c_str(), err);
printf("I2P: ERROR %s\n", lastError.c_str());
return false;
}
hProcess = pi.hProcess;
processId = pi.dwProcessId;
CloseHandle(pi.hThread);
// Kill i2pd if the wallet dies (matches the embedded Tor behaviour).
hJob = CreateJobObject(nullptr, nullptr);
if (hJob) {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo));
if (!AssignProcessToJobObject(hJob, hProcess))
printf("I2P: WARNING could not assign i2pd to Job Object (error %lu)\n", GetLastError());
}
printf("I2P: i2pd started (PID %lu)\n", processId);
#else
pid_t pid = fork();
if (pid < 0) {
lastError = "Failed to fork for i2pd process";
printf("I2P: ERROR %s\n", lastError.c_str());
return false;
}
if (pid == 0) {
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
execl(binaryPath.c_str(), binaryPath.c_str(),
"--conf", confPath.c_str(), (char*)nullptr);
_exit(1);
}
processId = pid;
printf("I2P: i2pd started (PID %d)\n", processId);
#endif
running = true;
// Wait for the SAM bridge to come up. The bridge opens quickly; tunnel
// build (needed for actual connectivity) continues in the background.
printf("I2P: waiting for SAM bridge on port %d...\n", samPort);
for (int i = 0; i < 45; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return false;
}
if (CanConnect("127.0.0.1", samPort)) {
printf("I2P: SAM bridge ready on port %d (took %ds)\n", samPort, i + 1);
return true;
}
if (!IsRunning()) {
lastError = "i2pd exited during start-up before the SAM bridge became ready";
printf("I2P: ERROR %s\n", lastError.c_str());
running = false;
return false;
}
}
lastError = strprintf("i2pd started but SAM port %d not ready after 45s", samPort);
printf("I2P: WARNING %s (it may still be building tunnels)\n", lastError.c_str());
return true;
}
void CI2PProcess::Stop()
{
if (fExternal) {
// We never launched it; leave the user's router running.
running = false;
return;
}
if (!running) return;
#ifdef WIN32
if (hProcess != nullptr) {
printf("I2P: stopping i2pd (PID %lu)...\n", processId);
TerminateProcess(hProcess, 0);
WaitForSingleObject(hProcess, 5000);
CloseHandle(hProcess);
hProcess = nullptr;
}
if (hJob != nullptr) {
CloseHandle(hJob);
hJob = nullptr;
}
#else
if (processId > 0) {
printf("I2P: stopping i2pd (PID %d)...\n", processId);
kill(processId, SIGTERM);
for (int i = 0; i < 50; i++) {
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
if (result != 0) break;
MilliSleep(100);
}
kill(processId, SIGKILL);
waitpid(processId, nullptr, 0);
}
#endif
processId = 0;
running = false;
printf("I2P: i2pd stopped\n");
}
bool CI2PProcess::IsRunning()
{
if (fExternal) return true;
if (!running) return false;
#ifdef WIN32
if (hProcess == nullptr) return false;
DWORD exitCode;
if (GetExitCodeProcess(hProcess, &exitCode))
return (exitCode == STILL_ACTIVE);
return false;
#else
if (processId <= 0) return false;
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
return (result == 0); // 0 => still running
#endif
}
bool StartEmbeddedI2P(const std::string& dataDir, int samPort)
{
return CI2PProcess::GetInstance()->Start(dataDir, samPort);
}
void StopEmbeddedI2P()
{
CI2PProcess::GetInstance()->Stop();
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright (c) 2024 Triangles developers
// I2P Router Process Manager - launches and manages a bundled i2pd binary
// Distributed under the MIT/X11 software license
//
// Mirrors tor_process.cpp: locate an i2pd executable shipped alongside the
// wallet (or installed on the system), write an auto-generated config that
// enables the SAM bridge, launch it as a managed child process, and shut it
// down when the wallet exits. The SAM session in i2p.cpp then connects to it,
// so the user does not have to install or run a separate I2P router.
#ifndef TRIANGLES_I2P_PROCESS_H
#define TRIANGLES_I2P_PROCESS_H
#include <string>
#ifdef WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
class CI2PProcess
{
public:
static CI2PProcess* GetInstance();
CI2PProcess();
~CI2PProcess();
// Bring up the router. If something is already listening on the SAM port we
// assume an external router and do not launch our own (fExternal=true).
// Returns true if a SAM bridge is (or will shortly be) reachable.
bool Start(const std::string& dataDir, int samPort = 7656);
// Terminate the launched router (no-op for an external one).
void Stop();
bool IsRunning();
bool IsExternal() const { return fExternal; }
std::string GetLastError() const { return lastError; }
std::string GetBinaryPath() const { return binaryPath; }
private:
std::string FindI2pdBinary();
bool WriteConfig();
static bool CanConnect(const std::string& host, int port);
int samPort;
bool running;
bool fExternal;
std::string dataDir;
std::string binaryPath;
std::string confPath;
std::string lastError;
#ifdef WIN32
HANDLE hProcess;
HANDLE hJob;
DWORD processId;
#else
int processId;
#endif
};
// Convenience wrappers for init.cpp.
bool StartEmbeddedI2P(const std::string& dataDir, int samPort);
void StopEmbeddedI2P();
#endif // TRIANGLES_I2P_PROCESS_H
+278 -36
View File
@@ -4,6 +4,8 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "txdb.h"
#include "walletdb.h"
#include "walletdb-recover.h" // BerkeleyRecoverWallet / BerkeleyZapWalletTx
#include "walletmigrate.h" // MaybeMigrateBerkeleyWalletToSQLite / IsSQLiteFile
#include "trianglesrpc.h"
#include "net.h"
#include "netbase.h"
@@ -19,6 +21,8 @@
#include "tor/tor_embedded.h"
#include "tor/onion_v3.h"
#include "tor/tor_process.h"
#include "i2p/i2p_embedded.h"
#include "i2p/i2pseed.h"
#ifdef ENABLE_ZMQ
#include "zmqpublishnotifier.h"
#endif
@@ -28,14 +32,23 @@
#include <memory>
#include <thread>
#include <vector>
// Forward declaration: InitError / InitWarning are defined further down
// in this file but referenced by AppInit (line ~423) before the definition.
static bool InitError(const std::string& str);
static bool InitWarning(const std::string& str);
#include <filesystem>
#include <fstream>
#include <boost/interprocess/sync/file_lock.hpp>
#include <algorithm>
#include <openssl/crypto.h>
#ifndef WIN32
#include <signal.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#endif
// Windows.h (transitively included) defines these as macros, clobbering Checkpoints:: enum values.
@@ -50,9 +63,41 @@
#endif
using namespace std;
using namespace boost;
namespace fs = std::filesystem;
namespace {
// Acquire an exclusive, non-blocking advisory lock on the datadir .lock file
// and hold it for the lifetime of the process. Replaces
// boost::interprocess::file_lock. The descriptor/handle is intentionally never
// released — the OS drops the lock automatically when the process exits.
bool LockDataDirectory(const std::filesystem::path& pathLockFile)
{
#ifdef WIN32
HANDLE hFile = CreateFileA(pathLockFile.string().c_str(),
GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return false;
OVERLAPPED ov = {};
if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0, MAXDWORD, MAXDWORD, &ov)) {
CloseHandle(hFile);
return false;
}
return true; // handle held until process exit
#else
int fd = open(pathLockFile.string().c_str(), O_RDWR | O_CREAT, 0644);
if (fd < 0)
return false;
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
close(fd);
return false;
}
return true; // fd held until process exit
#endif
}
} // namespace
std::unique_ptr<CWallet> pwalletMain;
CClientUIInterface uiInterface;
std::string strWalletFileName;
@@ -333,6 +378,9 @@ void Shutdown(void* parg)
pScriptCheckQueue.reset();
}
// Stop the embedded I2P router.
StopEmbeddedI2P();
// NOW safe to destroy Tor state - all threads have stopped
ShutdownTorV3();
StopEmbeddedTor();
@@ -413,6 +461,21 @@ bool AppInit(int argc, char* argv[])
}
ReadConfigFile(mapArgs, mapMultiArgs);
// AUDIT: If notorious=1 or -notor was set in triangles.conf, scream
// loudly. This is the silent path that put DNS2 on a 5+ day clearnet
// fork in 2026-06-23 — operator flipped it for troubleshooting, never
// reverted it, and the daemon happily started in clearnet-only mode.
// We refuse to proceed unless -recovery-mode=1 is ALSO set, even if
// the flag was set in the config file rather than on the command line.
if (mapArgs.count("-notor") && !GetBoolArg("-recovery-mode", false)) {
return InitError(_(
"-notor=1 found in triangles.conf or command line. Triangles is "
"Tor-native; running without Tor is unsafe and produces silent "
"clearnet forks (see 2026-06-23 DNS2 incident). If this is an "
"explicit recovery operation, pass -recovery-mode=1 on the command "
"line (in addition to the config file setting) to acknowledge."));
}
if (mapArgs.count("-?") || mapArgs.count("--help"))
{
// First part of help message is specific to trianglesd / RPC client
@@ -506,17 +569,22 @@ std::string HelpMessage()
" -dbcache=<n> " + _("Set database cache size in megabytes (default: 25)") + "\n" +
" -dblogsize=<n> " + _("Set database disk log size in megabytes (default: 100)") + "\n" +
" -timeout=<n> " + _("Specify connection timeout in milliseconds (default: 5000)") + "\n" +
" -torconnecttimeout=<n> " + _("Max time (ms) to wait for Tor to reach a peer .onion before giving up (default: 60000, range 5000-180000)") + "\n" +
" -torconnecttimeout=<n> " + _("Max time (ms) for the SOCKS5 handshake with the Tor proxy (send+recv of SOCKS5 init/auth/connect). Bounds how long a dead/slow .onion can stall the connector thread (default: 60000, range 5000-180000)") + "\n" +
//" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" +
//" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" +
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
" -notor " + _("Disable Tor - run in clearnet-only mode (no .onion connectivity)") + "\n" +
" -torsocks=<port> " + _("Set embedded or managed Tor SOCKS proxy port (default: 19099)") + "\n" +
" -torhiddenservice " + _("Enable the managed Tor hidden service (default: 1)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n"
" -i2p " + _("Enable embedded I2P router for .b32.i2p connectivity (default: 1)") + "\n"
" -i2psocks=<port> " + _("Set embedded I2P SOCKS proxy port (default: 19100)") + "\n"
" -i2psam=<port> " + _("Set embedded I2P SAM bridge port (default: 7656)") + "\n"
" -i2phsport=<port> " + _("Set I2P server tunnel forward port (default: wallet listen port)") + "\n" +
//" -dns " + _("Allow DNS lookups for -addnode, -seednode and -connect") + "\n" +
" -port=<port> " + _("Listen for connections on <port> (default: 24112 or testnet: 24111)") + "\n" +
" -maxconnections=<n> " + _("Maintain at most <n> connections to peers (default: 125)") + "\n" +
" -maxoutboundconnections=<n> " + _("Maximum outbound connections (default: 8, range 4-32)") + "\n" +
" -addnode=<ip> " + _("Add a node to connect to and attempt to keep the connection open") + "\n" +
" -connect=<ip> " + _("Connect only to the specified node(s)") + "\n" +
" -seednode=<ip> " + _("Connect to a node to retrieve peer addresses, and disconnect") + "\n" +
@@ -782,13 +850,18 @@ bool AppInit2()
}
// SOCKS5/Tor negotiation timeout. Separate from -timeout (which only covers
// the instant local connect to the Tor SOCKS proxy); this bounds how long we
// wait for Tor to reach the target .onion before giving up on that peer.
// the instant local connect to the Tor SOCKS proxy); this bounds the
// SOCKS5 handshake (send+recv of init/auth/connect). On a dead/slow .onion
// the recv() in Socks5() would otherwise block until Tor's own ~120s
// SocksTimeout fires, holding an outbound connection slot.
if (mapArgs.count("-torconnecttimeout"))
{
int nTorTimeout = GetArg("-torconnecttimeout", 60000);
if (nTorTimeout >= 5000 && nTorTimeout <= 180000)
if (IsValidSocksNegotiationTimeout(nTorTimeout))
nSocksNegotiationTimeout = nTorTimeout;
else
InitWarning("Ignoring -torconnecttimeout=" + mapArgs["-torconnecttimeout"] +
": out of range (5000..180000 ms), using default 60000");
}
if (mapArgs.count("-paytxfee"))
@@ -802,6 +875,15 @@ bool AppInit2()
fConfChange = GetBoolArg("-confchange", false);
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
// Validate -maxoutboundconnections (range 4-32, default 8)
if (mapArgs.count("-maxoutboundconnections"))
{
int nMaxOutboundConn = GetArg("-maxoutboundconnections", 8);
if (nMaxOutboundConn < 4 || nMaxOutboundConn > 32)
InitWarning("Ignoring -maxoutboundconnections=" + mapArgs["-maxoutboundconnections"] +
": out of range (4..32), using default 8");
}
int nScriptCheckThreads = GetArg("-par", 0);
if (nScriptCheckThreads <= 0)
nScriptCheckThreads = std::thread::hardware_concurrency();
@@ -842,8 +924,7 @@ bool AppInit2()
fs::path pathLockFile = GetDataDir() / ".lock";
FILE* file = fopen(pathLockFile.string().c_str(), "a"); // empty lock file; created if it doesn't exist.
if (file) fclose(file);
static boost::interprocess::file_lock lock(pathLockFile.string().c_str());
if (!lock.try_lock())
if (!LockDataDirectory(pathLockFile))
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. Triangles is probably already running."), strDataDir.c_str()));
#if !defined(WIN32) && !defined(QT_GUI)
@@ -889,6 +970,21 @@ bool AppInit2()
uiInterface.InitMessage(_("Verifying database integrity..."));
nStart = GetTimeMillis();
// The pre-rebase Berkeley-only paths (salvagewallet, zapwallettxes,
// bitdb.Verify, and the Berkeley→SQLite migration hook itself) only
// apply to a wallet.dat that is still a Berkeley DB file. Once the
// migration has run — or if the user is starting with a wallet that was
// already SQLite — those steps would either no-op or (worse) misinterpret
// the SQLite file as a corrupt Berkeley file and abort startup.
//
// The SQLite backend runs its own PRAGMA integrity_check in
// SQLiteDatabase::Open(), so the wallet is validated against the SQLite
// schema before the wallet handle is ever constructed downstream.
//
// Note: the snapshot is taken AFTER any migration hook below, so that
// post-migration the verify/salvage paths are skipped automatically.
bool walletIsSqlite = false;
if (!bitdb.Open(GetDataDir()))
{
string msg = strprintf(_("Error initializing database environment %s!"
@@ -899,33 +995,63 @@ bool AppInit2()
if (GetBoolArg("-salvagewallet"))
{
// Recover readable keypairs:
if (!CWalletDB::Recover(bitdb, strWalletFileName, true))
// Recover readable keypairs (Berkeley path; only relevant for legacy
// wallet.dat files that haven't been migrated to SQLite yet):
if (!BerkeleyRecoverWallet(bitdb, strWalletFileName, true))
return false;
}
if (GetBoolArg("-zapwallettxes") && fs::exists(GetDataDir() / strWalletFileName))
{
uiInterface.InitMessage(_("Zapping all transactions from wallet..."));
if (!CWalletDB::ZapWalletTx(strWalletFileName))
if (!BerkeleyZapWalletTx(strWalletFileName))
return InitError(_("Error: could not zap wallet transactions"));
}
if (fs::exists(GetDataDir() / strWalletFileName))
// ── Wallet backend migration ──────────────────────────────────────────────
// The daemon now defaults to SQLite (-walletdb=sqlite). If the wallet file
// on disk is still a Berkeley DB, convert it non-destructively to a SQLite
// wallet here, before the CWalletDB handle is opened downstream. The
// Berkeley original is preserved as "<name>.bdb.bak" alongside.
if (ResolveWalletDbKind() == WalletDbKind::SQLite &&
fs::exists(GetDataDir() / strWalletFileName) &&
!IsSQLiteFile(GetDataDir() / strWalletFileName))
{
CDBEnv::VerifyResult r = bitdb.Verify(strWalletFileName, CWalletDB::Recover);
if (r == CDBEnv::RECOVER_OK)
{
string msg = strprintf(_("Warning: wallet.dat corrupt, data salvaged!"
" Original wallet.dat saved as wallet.{timestamp}.bak in %s; if"
" your balance or transactions are incorrect you should"
" restore from a backup."), strDataDir.c_str());
uiInterface.ThreadSafeMessageBox(msg, _("Triangles"), CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
}
if (r == CDBEnv::RECOVER_FAIL)
return InitError(_("wallet.dat corrupt, salvage failed"));
uiInterface.InitMessage(_("Migrating wallet from Berkeley DB to SQLite..."));
std::string migErr;
if (!MaybeMigrateBerkeleyWalletToSQLite(GetDataDir() / strWalletFileName, migErr))
return InitError(_("Wallet migration failed: ") + migErr);
// Snapshot AFTER migration so the post-migration verify step below
// is skipped automatically when the wallet is now SQLite.
walletIsSqlite =
fs::exists(GetDataDir() / strWalletFileName) &&
IsSQLiteFile(GetDataDir() / strWalletFileName);
}
StartupPerfLog("verify_db", GetTimeMillis() - nStart, strprintf("wallet=%s", strWalletFileName.c_str()));
else
{
walletIsSqlite =
fs::exists(GetDataDir() / strWalletFileName) &&
IsSQLiteFile(GetDataDir() / strWalletFileName);
}
if (!walletIsSqlite)
{
if (fs::exists(GetDataDir() / strWalletFileName))
{
CDBEnv::VerifyResult r = bitdb.Verify(strWalletFileName, BerkeleyRecoverWallet);
if (r == CDBEnv::RECOVER_OK)
{
string msg = strprintf(_("Warning: wallet.dat corrupt, data salvaged!"
" Original wallet.dat saved as wallet.{timestamp}.bak in %s; if"
" your balance or transactions are incorrect you should"
" restore from a backup."), strDataDir.c_str());
uiInterface.ThreadSafeMessageBox(msg, _("Triangles"), CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
}
if (r == CDBEnv::RECOVER_FAIL)
return InitError(_("wallet.dat corrupt, salvage failed"));
}
}
StartupPerfLog("verify_db", GetTimeMillis() - nStart, strprintf("wallet=%s wallet_is_sqlite=%d", strWalletFileName.c_str(), (int)walletIsSqlite));
// ********************************************************* Step 6: network initialization
nStart = GetTimeMillis();
@@ -1141,14 +1267,32 @@ bool AppInit2()
}
}
// ********************************************************* Step 6d: optional LevelDB -> RocksDB chain DB migration
if (GetBoolArg("-migratechaindb", false) || GetBoolArg("-migratechaindbforce", false))
// ********************************************************* Step 6d: LevelDB -> RocksDB chain DB migration
// Runs when explicitly requested (-migratechaindb[force]) OR automatically
// when RocksDB is the active backend and the only chain DB present is a
// legacy LevelDB (txleveldb). This makes the RocksDB default transparent
// for existing nodes: their chain state is copied (and verified) into a new
// rocksdb/ directory on first launch, leaving the LevelDB source untouched
// as a fallback. MaybeMigrateLevelDbToRocksDb() is a no-op when there is no
// LevelDB source or a RocksDB directory already exists, so it is safe to
// call on every startup.
{
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
std::string strMigrateError;
bool fForce = GetBoolArg("-migratechaindbforce", false);
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
bool fExplicit = GetBoolArg("-migratechaindb", false) ||
GetBoolArg("-migratechaindbforce", false);
bool fAuto = IsRocksDbChainBackend() &&
fs::exists(GetDataDir() / "txleveldb") &&
!fs::exists(GetDataDir() / "rocksdb");
if (fExplicit || fAuto)
{
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
if (fAuto && !fExplicit)
printf("ChainDB: RocksDB backend active with a legacy LevelDB present; "
"migrating automatically.\n");
std::string strMigrateError;
bool fForce = GetBoolArg("-migratechaindbforce", false);
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
}
}
// ********************************************************* Step 7: load blockchain
@@ -1491,11 +1635,26 @@ bool AppInit2()
fUseUPnP = false;
#endif
} else if (GetBoolArg("-notor", false)) {
// -notor: user explicitly disabled Tor. Allow the daemon to start
// in clearnet-only mode (useful for diagnostics, benchmarking, and
// recovery). .onion connectivity will not be available.
printf("NOTICE: Tor disabled via -notor. Running in clearnet-only mode.\n");
// -notor: explicit clearnet mode. Triangles is Tor-native and
// running without Tor is unsafe for normal operation — it can
// produce silent clearnet forks (see 2026-06-23 DNS2 incident,
// 5+ days on a parallel chain because -notor=1 was left on after
// troubleshooting). The flag is preserved for explicit recovery
// workflows (e.g. dumputxoset-from-clearnet when bootstrapping
// a new node) but requires an additional -recovery-mode=1
// confirmation flag so it cannot be flipped by accident.
if (!GetBoolArg("-recovery-mode", false)) {
return InitError(_(
"-notor requires -recovery-mode=1 confirmation. Triangles is Tor-native; "
"running without Tor is unsafe and produces silent clearnet forks. "
"If you need clearnet mode for bootstrap recovery or diagnostics, "
"pass BOTH -notor=1 -recovery-mode=1 on the command line."));
}
printf("WARNING: Tor disabled via -notor AND -recovery-mode=1 set. "
"Running in clearnet-only mode.\n");
printf(" .onion connections will NOT be available.\n");
printf(" This mode is for RECOVERY ONLY — exit and restart without these\n"
" flags as soon as the recovery operation completes.\n");
SetReachable(NET_IPV4, true);
SetReachable(NET_IPV6, true);
SetReachable(NET_TOR, false);
@@ -1506,6 +1665,47 @@ bool AppInit2()
return InitError(strprintf(_("Tor failed to start. Triangles requires Tor to operate.\n\nDetails: %s"), torError.c_str()));
}
// ════════════════════════════════════════════════════════════════
// Embedded I2P (i2pd) startup
//
// I2P runs as a co-equal anonymity network alongside Tor. When Tor
// starts successfully (tor-native mode), I2P provides an alternative
// anonymous transport via .b32.i2p destinations. When Tor is disabled
// (-notor recovery mode), I2P is still started to maintain anonymity.
//
// I2P's SOCKS proxy (default 19100) handles outbound .i2p connections.
// A server tunnel forwards incoming I2P connections to the P2P port.
// ════════════════════════════════════════════════════════════════
if (torStarted || GetBoolArg("-notor", false)) {
uiInterface.InitMessage(_("Starting embedded I2P router..."));
int64_t nI2PStart = GetTimeMillis();
bool i2pStarted = StartEmbeddedI2P();
StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart,
strprintf("started=%d", i2pStarted));
if (i2pStarted) {
int i2pSocksPort = CI2PEmbedded::GetInstance()->GetSocksPort();
CService i2pProxyAddr("127.0.0.1", i2pSocksPort);
// Route I2P traffic through i2pd's SOCKS proxy
SetProxy(NET_I2P, i2pProxyAddr, 5);
SetReachable(NET_I2P, true);
printf("I2P-NATIVE MODE: I2P router running\n");
printf(" SOCKS proxy at 127.0.0.1:%d for .b32.i2p connections\n",
i2pSocksPort);
printf(" Dual-network anonymity: Tor (.onion) + I2P (.b32.i2p)\n");
} else {
// I2P failure is non-fatal — Tor-only operation continues.
// The daemon still works with .onion peers.
std::string i2pError = CI2PEmbedded::GetInstance()->GetStartupError();
printf("WARNING: Embedded I2P did not start. Running Tor-only.\n");
if (!i2pError.empty())
printf(" I2P error: %s\n", i2pError.c_str());
SetReachable(NET_I2P, false);
}
}
// Initialize Tor V3 identity (Ed25519 keys, onion address)
uiInterface.InitMessage(_("Initializing Tor V3 identity..."));
printf("Initializing Tor V3 onion identity...\n");
@@ -1572,6 +1772,26 @@ bool AppInit2()
if (!NewThread(ThreadTorMaintenance, nullptr))
printf("Warning: ThreadTorMaintenance could not be started\n");
}
// Bring up I2P (SAM) transport alongside Tor so the wallet has both a
// .onion and a .b32.i2p address. On by default; disable with -i2p=0.
// A bundled i2pd router is launched automatically (mirroring embedded
// Tor); if -i2psam points at a non-loopback bridge, or a router is
// already running, we use that instead.
if (GetBoolArg("-i2p", true)) {
int64_t nI2PStart = GetTimeMillis();
uiInterface.InitMessage(_("Starting the I2P router..."));
bool i2pStarted = StartEmbeddedI2P();
StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart, strprintf("started=%d", i2pStarted));
if (i2pStarted) {
SetReachable(NET_I2P, true);
std::string i2pAddr = CI2PEmbedded::GetInstance()->GetI2PAddress();
printf("I2P network enabled. Our address: %s\n", i2pAddr.c_str());
} else {
printf("NOTICE: I2P not available this session; continuing with Tor only\n");
}
}
}
// ********************************************************* Step 9: import blocks
@@ -1620,6 +1840,28 @@ bool AppInit2()
printf("Loaded %i addresses from peers.dat %" PRId64 "ms\n",
addrman.size(), GetTimeMillis() - nStart);
StartupPerfLog("peers_load", GetTimeMillis() - nStart, strprintf("count=%d", addrman.size()));
// Add hardcoded I2P (.b32.i2p) seed addresses to the address manager.
// This enables cross-network peer discovery: Tor-connected nodes can learn
// about I2P peers and vice versa. Onion seeds are loaded separately in
// ThreadOnionSeed (net.cpp), but we add I2P seeds here during init so they
// are available immediately for the outbound connector.
{
static const char *(*strI2PSeed)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
int nI2PSeeds = 0;
for (unsigned int si = 0; strI2PSeed[si][0] != nullptr; si++) {
CNetAddr parsed;
if (parsed.SetSpecial(strI2PSeed[si][0])) {
int nOneDay = 24 * 3600;
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
addr.nTime = GetTime() - 3 * nOneDay - GetRand(4 * nOneDay);
addrman.Add(addr, parsed);
nI2PSeeds++;
}
}
if (nI2PSeeds > 0)
printf("Added %d hardcoded I2P (.b32.i2p) seed addresses to addrman\n", nI2PSeeds);
}
// ********************************************************* Step 11: start node
+756 -156
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -58,11 +58,17 @@ public:
TxPriorityCompare(bool _byFee) : byFee(_byFee) { }
bool operator()(const TxPriority& a, const TxPriority& b)
{
// #8: Fee-weighted priority for PoS staking.
// When sorting by fee (PoS mode), apply a 2x weight to fees so
// higher-fee transactions are prioritized over coin-age-only ones.
// This maximizes staking rewards for the minter.
if (byFee)
{
if (std::get<1>(a) == std::get<1>(b))
double feeA = std::get<1>(a) * 2.0; // fee boost
double feeB = std::get<1>(b) * 2.0;
if (feeA == feeB)
return std::get<0>(a) < std::get<0>(b);
return std::get<1>(a) < std::get<1>(b);
return feeA < feeB;
}
else
{
+374 -55
View File
@@ -11,6 +11,9 @@
#include "addrman.h"
#include "ui_interface.h"
#include "onionseed.h"
#include "tor/onion_v3.h"
#include "snapshotnet.h"
#include "i2p/i2pseed.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
@@ -19,6 +22,8 @@
#ifdef WIN32
#include <string.h>
#else
#include <sys/uio.h>
#endif
#ifdef USE_UPNP
@@ -36,7 +41,9 @@ extern "C" {
// int tor_main(int argc, char *argv[]);
}
static const int MAX_OUTBOUND_CONNECTIONS = 8; // reduced from 16 for Tor-only small networks
// Configurable max outbound connections. Set from -maxoutboundconnections
// during network init (StartNode). Default 8, configurable range 4-32.
static int MAX_OUTBOUND_CONNECTIONS = 8;
void ThreadMessageHandler2(void* parg);
void ThreadSocketHandler2(void* parg);
@@ -327,6 +334,86 @@ bool IsReachable(const CNetAddr& addr)
return vfReachable[net] && !vfLimited[net];
}
// ────────────────────────────────────────────────────────────────────────────
// Cross-network Tor ↔ I2P peer discovery helpers
// ────────────────────────────────────────────────────────────────────────────
/**
* Check whether a CAddress refers to an I2P (.b32.i2p) endpoint.
* Returns true if the string representation of the address contains ".i2p".
*/
bool IsI2PAddr(const CAddress& addr)
{
std::string addrStr = addr.ToStringIP();
return (addrStr.find(".i2p") != std::string::npos);
}
/**
* Check whether a CAddress refers to a Tor (.onion) endpoint.
*/
static bool IsOnionAddr(const CAddress& addr)
{
std::string addrStr = addr.ToStringIP();
return (addrStr.find(".onion") != std::string::npos);
}
/**
* Cross-network address relay: when an 'addr' message is received from a
* peer on one anonymity network, this function bridges addresses belonging
* to the *other* network to the appropriate peers.
*
* - .b32.i2p addresses received from any peer relay to I2P-connected peers
* - .onion addresses received from any peer relay to Tor-connected peers
*
* This breaks the isolation between Tor and I2P peer sets so that a Tor
* node can learn about I2P peers and vice versa.
*/
void RelayCrossNetworkAddr(const std::vector<CAddress>& vAddr)
{
bool hasI2P = false;
bool hasOnion = false;
for (const CAddress& addr : vAddr) {
if (IsI2PAddr(addr)) hasI2P = true;
if (IsOnionAddr(addr)) hasOnion = true;
}
if (!hasI2P && !hasOnion)
return;
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode->fDisconnect)
continue;
std::string peerAddr = pnode->addr.ToStringIP();
bool peerIsI2P = (peerAddr.find(".i2p") != std::string::npos);
bool peerIsOnion = (peerAddr.find(".onion") != std::string::npos);
for (const CAddress& addr : vAddr) {
// Bridge I2P addresses to I2P peers
if (hasI2P && IsI2PAddr(addr) && peerIsI2P) {
pnode->PushAddress(addr);
}
// Bridge .onion addresses to Tor peers
if (hasOnion && IsOnionAddr(addr) && peerIsOnion) {
pnode->PushAddress(addr);
}
// Cross-bridge: also push I2P addresses to Tor peers and
// .onion addresses to I2P peers so each network learns about
// the other's peers.
if (hasI2P && IsI2PAddr(addr) && peerIsOnion) {
pnode->PushAddress(addr);
}
if (hasOnion && IsOnionAddr(addr) && peerIsI2P) {
pnode->PushAddress(addr);
}
}
}
if (fDebug && (hasI2P || hasOnion))
printf("RelayCrossNetworkAddr: bridged %s%s%s addresses across networks\n",
hasOnion ? ".onion " : "", hasI2P ? ".i2p " : "",
(hasOnion && hasI2P) ? "(both)" : "");
}
bool GetMyExternalIP2(const CService& addrConnect, const char* pszGet, const char* pszKeyword, CNetAddr& ipRet)
{
SOCKET hSocket;
@@ -494,11 +581,13 @@ CNode* FindNode(const CService& addr)
CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
{
// TOR-NATIVE: Reject all non-.onion addresses
// TOR+I2P NATIVE: Reject all clearnet (non-.onion, non-.b32.i2p) addresses
std::string addrStr = pszDest ? std::string(pszDest) : addrConnect.ToStringIP();
if (addrStr.find(".onion") == std::string::npos) {
bool isOnion = (addrStr.find(".onion") != std::string::npos);
bool isI2P = (addrStr.find(".i2p") != std::string::npos);
if (!isOnion && !isI2P) {
if (fDebug)
printf("ConnectNode(): REJECTED non-onion address: %s (Tor-native mode)\n", addrStr.c_str());
printf("ConnectNode(): REJECTED clearnet address: %s (Tor/I2P native mode)\n", addrStr.c_str());
return nullptr;
}
@@ -561,6 +650,54 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
}
}
// Adopt a connected I2P SAM data socket (from the accept loop in i2p.cpp) as an
// inbound peer. The socket arrives in blocking mode; switch it to non-blocking
// to match the rest of the socket handler, then register the node.
void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr)
{
if (hSocket == INVALID_SOCKET)
return;
if (CNode::IsBanned(addr)) {
printf("I2P inbound from %s dropped (banned)\n", addr.ToString().c_str());
closesocket(hSocket);
return;
}
// Honour the inbound connection limit.
int nInbound = 0;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
if (pnode->fInbound)
nInbound++;
}
int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS;
if (nInbound >= nMaxInbound) {
printf("I2P inbound from %s dropped (too many inbound)\n", addr.ToString().c_str());
closesocket(hSocket);
return;
}
#ifdef WIN32
u_long nOne = 1;
if (ioctlsocket(hSocket, FIONBIO, &nOne) == SOCKET_ERROR)
printf("AddI2PInboundNode() : ioctlsocket non-blocking setting failed, error %d\n", WSAGetLastError());
#else
if (fcntl(hSocket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR)
printf("AddI2PInboundNode() : fcntl non-blocking setting failed, error %d\n", errno);
#endif
printf("accepted I2P connection %s\n", addr.ToString().c_str());
CNode* pnode = new CNode(hSocket, addr, "", true);
pnode->AddRef();
pnode->nTimeConnected = GetTime();
{
LOCK(cs_vNodes);
vNodes.push_back(pnode);
}
}
void CNode::CloseSocketDisconnect()
{
fDisconnect = true;
@@ -827,36 +964,96 @@ void SocketSendData(CNode *pnode)
std::deque<CSerializeData>::iterator it = pnode->vSendMsg.begin();
while (it != pnode->vSendMsg.end()) {
#ifndef WIN32
// Coalesce up to MAX_IOV queued messages into a single syscall using
// scatter-gather I/O. On Linux we use sendmsg() so we can pass
// MSG_NOSIGNAL | MSG_DONTWAIT; on other POSIX systems (e.g. BSD where
// SO_NOSIGPIPE is already set on the socket) we fall back to writev().
static const int MAX_IOV = 16;
struct iovec iov[MAX_IOV];
int iovcnt = 0;
std::deque<CSerializeData>::iterator batchEnd = it;
for (; batchEnd != pnode->vSendMsg.end() && iovcnt < MAX_IOV; ++batchEnd, ++iovcnt) {
const CSerializeData &data = *batchEnd;
size_t off = (batchEnd == it) ? pnode->nSendOffset : 0;
assert(data.size() > off);
iov[iovcnt].iov_base = const_cast<char*>(&data[off]);
iov[iovcnt].iov_len = data.size() - off;
}
if (iovcnt == 0)
break;
ssize_t nBytes;
#ifdef MSG_NOSIGNAL
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = iovcnt;
nBytes = sendmsg(pnode->hSocket, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
#else
nBytes = writev(pnode->hSocket, iov, iovcnt);
#endif
if (nBytes > 0) {
pnode->nLastSend = GetTime();
pnode->nSendBytes += nBytes;
// Consume nBytes across the coalesced messages
while (it != batchEnd && nBytes > 0) {
const CSerializeData &data = *it;
size_t remaining = data.size() - pnode->nSendOffset;
if ((size_t)nBytes >= remaining) {
nBytes -= remaining;
pnode->nSendSize -= data.size();
pnode->nSendOffset = 0;
++it;
} else {
pnode->nSendOffset += nBytes;
nBytes = 0;
}
}
// Socket buffer full mid-batch — wait for next cycle
if (it != batchEnd)
break;
} else if (nBytes < 0) {
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) {
printf("socket send error %d\n", nErr);
pnode->CloseSocketDisconnect();
}
break;
} else {
// nBytes == 0: peer closed
break;
}
#else
// Windows: individual send() calls
const CSerializeData &data = *it;
assert(data.size() > pnode->nSendOffset);
int nBytes = send(pnode->hSocket, &data[pnode->nSendOffset], data.size() - pnode->nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT);
if (nBytes > 0) {
pnode->nLastSend = GetTime();
pnode->nSendOffset += nBytes;
pnode->nSendBytes += nBytes;
pnode->nSendBytes += nBytes;
if (pnode->nSendOffset == data.size()) {
pnode->nSendOffset = 0;
pnode->nSendSize -= data.size();
it++;
} else {
// could not send full message; stop sending more
break;
}
} else {
if (nBytes < 0) {
// error
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
{
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) {
printf("socket send error %d\n", nErr);
pnode->CloseSocketDisconnect();
}
}
// couldn't send anything at all
break;
}
#endif
}
if (it == pnode->vSendMsg.end()) {
@@ -1090,6 +1287,16 @@ void ThreadSocketHandler2(void* parg)
break;
}
}
// Also check I2P seed addresses
if (!fIsSeed) {
static const char *(*strI2PSeedCheck)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
for (unsigned int si = 0; strI2PSeedCheck[si][0] != nullptr; si++) {
if (incomingAddr.find(strI2PSeedCheck[si][0]) != std::string::npos) {
fIsSeed = true;
break;
}
}
}
if (fIsSeed && nInbound < nMaxInbound + 2) {
fAccept = true;
printf("accepted seed node %s (reserved slot)\n", addr.ToString().c_str());
@@ -1217,7 +1424,7 @@ void ThreadSocketHandler2(void* parg)
if (fShutdown)
return;
MilliSleep(10);
MilliSleep(IsInitialBlockDownload() ? 1 : 10);
}
}
@@ -1445,6 +1652,39 @@ void ThreadOnionSeed(void* parg)
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
int found = 0;
// Defense-in-depth (2026-06-22): Validate every hardcoded seed against the
// v3 onion checksum BEFORE we hand it to Tor. The btb6/gtb6 incident
// (4,842 "No more HSDir" errors over a 12h from-zero sync test) was caused
// by a single-character corruption that Tor rejected with a cryptic
// "ed25519 validation failed" warning. Catching it here gives the operator
// a clear, actionable error at startup with no wasted network/CPU.
// See references/onion-corruption-ci-defense.md (CI Layers 2-3) for the
// static-analysis side of this defense.
{
int nInvalid = 0;
int nTotal = 0;
std::string strFirstBad;
for (unsigned int si = 0; strOnionSeed[si][0] != nullptr; si++) {
nTotal++;
if (!CTorV3Service::ValidateOnionAddress(strOnionSeed[si][0])) {
if (strFirstBad.empty()) strFirstBad = strOnionSeed[si][0];
nInvalid++;
}
}
if (nInvalid > 0) {
std::string strErr = strprintf(
"ThreadOnionSeed() : %d of %d hardcoded .onion seed(s) failed v3 "
"checksum validation. First bad address: %s. "
"This is the btb6/gtb6 class of bug (see references/onion-corruption-ci-defense.md). "
"Fix src/onionseed.h before starting the daemon — Tor would "
"have wasted hours producing cryptic 'ed25519 validation failed' "
"warnings otherwise.",
nInvalid, nTotal, strFirstBad.c_str());
printf("ERROR: %s\n", strErr.c_str());
throw runtime_error(strErr);
}
}
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) {
CNetAddr parsed;
if (!parsed.SetSpecial(strOnionSeed[seed_idx][0]))
@@ -1465,6 +1705,31 @@ void ThreadOnionSeed(void* parg)
printf("%d addresses from hardcoded .onion seeds (queued as OneShot)\n", found);
// Load hardcoded I2P (.b32.i2p) seeds for cross-network peer discovery.
// These are added to the address manager so that I2P-connected peers can
// be discovered. Unlike onion seeds, we don't queue them as OneShot
// connections here — they're connected via the normal outbound connector
// through the I2P SOCKS proxy.
{
static const char *(*strI2PSeed)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
int i2pFound = 0;
for (unsigned int si = 0; strI2PSeed[si][0] != nullptr; si++) {
CNetAddr parsed;
if (!parsed.SetSpecial(strI2PSeed[si][0])) {
printf("WARNING: ThreadOnionSeed() : invalid .b32.i2p seed: %s\n",
strI2PSeed[si][0]);
continue;
}
int nOneDay = 24*3600;
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay);
addrman.Add(addr, parsed);
i2pFound++;
}
if (i2pFound > 0)
printf("%d addresses from hardcoded .b32.i2p seeds added to addrman\n", i2pFound);
}
// Wait for Tor to establish circuits before attempting HTTPS seed fetch.
// The hardcoded OneShot connections can race ahead meanwhile.
printf("ThreadOnionSeed: waiting 20s for Tor circuits before HTTPS seed fetch...\n");
@@ -1750,6 +2015,10 @@ bool ThreadHTTPSeedFetch2(void* parg)
// body then carries hex chunk-size lines interleaved with the data; parsing
// it raw fuses a chunk marker onto an address and we lose most of the list
// (the classic "only 1 address" symptom). De-chunk first when present.
//
// v5.9.22 hardening: the parser is now strict and reports a distinct
// failure code for each kind of malformed framing. See DechunkResult in
// netbase.h and the unit tests in src/test/http_seed_tests.cpp.
{
std::string h = headers;
for (char& c : h) c = (char)tolower((unsigned char)c);
@@ -1757,22 +2026,20 @@ bool ThreadHTTPSeedFetch2(void* parg)
h.find("chunked") != std::string::npos)
{
std::string decoded;
size_t pos = 0;
while (pos < body.size()) {
size_t eol = body.find("\r\n", pos);
if (eol == std::string::npos) break;
std::string sizeLine = body.substr(pos, eol - pos);
size_t semi = sizeLine.find(';'); // strip chunk extensions
if (semi != std::string::npos) sizeLine = sizeLine.substr(0, semi);
unsigned long chunkSize = strtoul(sizeLine.c_str(), nullptr, 16);
pos = eol + 2;
if (chunkSize == 0) break; // last chunk
if (pos + chunkSize > body.size())
chunkSize = body.size() - pos; // defensive clamp
decoded.append(body, pos, chunkSize);
pos += chunkSize;
if (pos + 2 <= body.size() && body.compare(pos, 2, "\r\n") == 0)
pos += 2; // trailing CRLF after data
int rc = DechunkTransferEncoding(body, decoded);
if (rc != DECHUNK_OK) {
const char* reason = "unknown";
switch (rc) {
case DECHUNK_EMPTY: reason = "empty body"; break;
case DECHUNK_NO_CHUNK_TERMINATOR: reason = "missing chunk terminator (CRLF)"; break;
case DECHUNK_INVALID_HEX: reason = "malformed chunk-size (not valid hex)"; break;
case DECHUNK_OVERSIZE_CHUNK: reason = "chunk size exceeds remaining input (truncated)"; break;
case DECHUNK_MISSING_DATA_CRLF: reason = "missing CRLF after chunk data"; break;
default: reason = "unknown"; break;
}
printf("HTTPS seed fetch: malformed chunked transfer encoding (%s) from %s\n",
reason, seedHost.c_str());
return false;
}
body.swap(decoded);
}
@@ -1783,7 +2050,11 @@ bool ThreadHTTPSeedFetch2(void* parg)
// Tolerant parse: accept one-per-line OR several addresses on one line
// (whitespace / comma / semicolon separated), and ignore inline '#' comments.
// v5.9.22: the splitting logic is now a pure function in netbase.cpp so
// we can unit-test every line format. The CNetAddr/CService/addrman
// validation stays here because it touches globals.
int found = 0;
int skipped = 0;
auto addSeed = [&](std::string addrStr) -> void {
while (!addrStr.empty() && (addrStr.back()=='\r' || addrStr.back()==' ' || addrStr.back()=='\t'))
@@ -1795,11 +2066,16 @@ bool ThreadHTTPSeedFetch2(void* parg)
int port = GetDefaultPort();
size_t onionPos = addrStr.find(".onion:");
size_t i2pPos = addrStr.find(".i2p:");
if (onionPos != std::string::npos) {
port = atoi(addrStr.substr(onionPos + 7).c_str());
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
} else if (addrStr.find(".onion") == std::string::npos) {
return; // Tor-native: skip non-.onion addresses
} else if (i2pPos != std::string::npos) {
port = atoi(addrStr.substr(i2pPos + 5).c_str());
// keep the ".i2p" suffix
} else if (addrStr.find(".onion") == std::string::npos &&
addrStr.find(".i2p") == std::string::npos) {
return; // Tor/I2P-native: skip clearnet addresses
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
@@ -1811,38 +2087,34 @@ bool ThreadHTTPSeedFetch2(void* parg)
addrman.Add(addr, service);
printf("HTTPS seed: added %s:%d\n", addrStr.c_str(), port);
found++;
} else {
skipped++;
}
};
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line))
// Use the pure helper to split the body. If it returns nothing, that
// means the body was entirely comments / blank lines / whitespace —
// distinct failure mode worth logging separately from "no valid
// addresses after parsing".
std::vector<std::string> tokens = ParseSeedListBody(body);
if (tokens.empty()) {
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
return false;
}
for (const std::string& tok : tokens)
{
if (fShutdown)
return false;
// Strip inline comments (everything from '#' onward)
size_t hashPos = line.find('#');
if (hashPos != std::string::npos)
line = line.substr(0, hashPos);
// Split on whitespace / comma / semicolon so multiple addresses on
// one line are all captured.
size_t start = 0;
while (start <= line.size()) {
size_t sep = line.find_first_of(" \t,;", start);
std::string tok = (sep == std::string::npos)
? line.substr(start)
: line.substr(start, sep - start);
if (!tok.empty())
addSeed(tok);
if (sep == std::string::npos) break;
start = sep + 1;
}
addSeed(tok);
}
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
return found > 0;
if (found == 0) {
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
return false;
}
return true;
} catch (std::exception& e) {
printf("HTTPS seed fetch failed: %s\n", e.what());
@@ -2515,8 +2787,25 @@ void StartNode(void* parg)
// Make this thread recognisable as the startup thread
RenameThread("Triangles-start");
// Configurable outbound connections via -maxoutboundconnections (default 8, range 4-32)
MAX_OUTBOUND_CONNECTIONS = GetArg("-maxoutboundconnections", 8);
if (MAX_OUTBOUND_CONNECTIONS < 4) MAX_OUTBOUND_CONNECTIONS = 4;
if (MAX_OUTBOUND_CONNECTIONS > 32) MAX_OUTBOUND_CONNECTIONS = 32;
printf("Configured max outbound connections: %d (from -maxoutboundconnections)\n", MAX_OUTBOUND_CONNECTIONS);
// If a canonical UTXO snapshot file is already present at startup,
// advertise NODE_SNAPSHOT to peers BEFORE the first outbound connection.
// EnsureLocalSnapshot() also sets this flag post-IBD, but at that point
// already-connected peers have already cached our version message and
// won't re-read our service bits — so for the "place canonical file in
// datadir before launch" operator workflow this pre-handshake OR is the
// load-bearing one.
if (!fClient) {
SnapshotNet::EnsureLocalSnapshot();
}
if (semOutbound == nullptr) {
// initialize semaphore — use -maxoutbound if specified, else default
// initialize semaphore — use -maxoutboundconnections (set above), fall back to -maxoutbound
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
nMaxOutbound = max(nMaxOutbound, 1); // at least 1 outbound
@@ -2568,6 +2857,10 @@ void StartNode(void* parg)
if (!NewThread(ThreadOpenConnections, nullptr))
printf("Error: NewThread(ThreadOpenConnections) failed\n");
// Start fork detector (post-IBD background monitor)
if (!NewThread(ThreadForkDetector, nullptr))
printf("Error: NewThread(ThreadForkDetector) failed\n");
// Process messages
if (!NewThread(ThreadMessageHandler, nullptr))
printf("Error: NewThread(ThreadMessageHandler) failed\n");
@@ -2702,3 +2995,29 @@ void RelayTransaction(const CTransaction& tx, const uint256& hash, const CDataSt
RelayInventory(inv);
}
// ---------------------------------------------------------------------------
// BIP152 Compact Block relay — net-layer integration
// ---------------------------------------------------------------------------
/** Advertise a new block to all connected peers.
*
* For peers that have negotiated compact block relay (fSendCmpct), the
* inventory is sent as MSG_CMPCT_BLOCK so they know to request the compact
* form. For legacy peers, standard MSG_BLOCK inventory is sent.
*
* The actual compact block construction and sending happens in main.cpp
* (SendCompactBlock / ProcessCompactBlock). This function only handles
* the inventory advertisement at the net layer.
*/
void RelayBlockInventory(const uint256& hash)
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
// Use MSG_CMPCT_BLOCK for peers that support compact relay,
// MSG_BLOCK for legacy peers.
int nType = pnode->fSendCmpct ? MSG_CMPCT_BLOCK : MSG_BLOCK;
pnode->PushInventory(CInv(nType, hash));
}
}
+4
View File
@@ -21,7 +21,9 @@
class CNode;
class CBlockIndex;
bool IsInitialBlockDownload();
void ThreadForkDetector(void*);
extern int nBestHeight;
extern int nForkAlertCount;
@@ -35,6 +37,8 @@ void AddressCurrentlyConnected(const CService& addr);
CNode* FindNode(const CNetAddr& ip);
CNode* FindNode(const CService& ip);
CNode* ConnectNode(CAddress addrConnect, const char *strDest = nullptr);
// Adopt a connected I2P SAM data socket as an inbound peer (called from i2p.cpp).
void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr);
void MapPort();
unsigned short GetListenPort();
bool BindListenPort(const CService &bindAddr, std::string& strError=REF(std::string()));
+259 -10
View File
@@ -10,8 +10,15 @@
#ifndef WIN32
#include <sys/fcntl.h>
#include <netinet/tcp.h>
#endif
#include <cstdlib>
#include <cctype>
#include <cerrno>
#include <limits>
#include <sstream>
#include "strlcpy.h"
using namespace std;
@@ -451,6 +458,19 @@ bool static ConnectSocketDirectly(const CService &addrConnect, SOCKET& hSocketRe
}
}
// TCP_NODELAY: disable Nagle's algorithm for low-latency P2P messaging.
// SO_KEEPALIVE: detect dead connections faster (important for Tor/I2P
// tunnels that can silently drop without RST/FIN).
{
int one = 1;
#ifdef WIN32
setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&one, sizeof(one));
#else
setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
#endif
setsockopt(hSocket, SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one));
}
// this isn't even strictly necessary
// CNode::ConnectNode immediately turns the socket back to non-blocking
// but we'll turn it back to blocking just in case
@@ -585,6 +605,33 @@ bool ConnectSocketByName(CService &addr, SOCKET& hSocketRet, const char *pszDest
SOCKET hSocket = INVALID_SOCKET;
// I2P routing: .b32.i2p destinations go through i2pd's SOCKS proxy, not
// the Tor name proxy. This is the key routing decision for dual-network
// anonymity — Tor handles .onion, i2pd handles .b32.i2p.
bool isI2PDest = (strDest.size() > 7 &&
strDest.substr(strDest.size() - 7, 7) == ".b32.i2p");
if (isI2PDest) {
// Route through the I2P SOCKS proxy
proxyType i2pProxy;
if (GetProxy(NET_I2P, i2pProxy)) {
addr = CService("0.0.0.0:0");
printf("ConnectSocketByName(): routing .b32.i2p via I2P SOCKS proxy\n");
if (!ConnectSocketDirectly(i2pProxy.first, hSocket, nTimeout))
return false;
// i2pd's SOCKS proxy accepts .b32.i2p domain names via SOCKS5 ATYP=domain
if (!Socks5(strDest, port, hSocket)) {
printf("ConnectSocketByName(): I2P SOCKS5 handshake failed\n");
return false;
}
printf("ConnectSocketByName(): connected via I2P SOCKS5\n");
hSocketRet = hSocket;
return true;
}
// No I2P proxy configured — fall through to nameproxy (will likely fail)
printf("ConnectSocketByName(): WARNING - .b32.i2p dest but no I2P proxy set\n");
}
proxyType nameproxy;
GetNameProxy(nameproxy);
@@ -625,6 +672,7 @@ void CNetAddr::Init()
memset(ip, 0, sizeof(ip));
memset(tor_v3_pubkey, 0, sizeof(tor_v3_pubkey));
m_is_tor_v3 = false;
m_is_i2p = false;
}
void CNetAddr::SetIP(const CNetAddr& ipIn)
@@ -632,6 +680,7 @@ void CNetAddr::SetIP(const CNetAddr& ipIn)
memcpy(ip, ipIn.ip, sizeof(ip));
memcpy(tor_v3_pubkey, ipIn.tor_v3_pubkey, sizeof(tor_v3_pubkey));
m_is_tor_v3 = ipIn.m_is_tor_v3;
m_is_i2p = ipIn.m_is_i2p;
}
static const unsigned char pchOnionCat[] = {0xFD,0x87,0xD8,0x7E,0xEB,0x43};
@@ -666,13 +715,41 @@ bool CNetAddr::SetSpecial(const std::string &strName)
m_is_tor_v3 = false;
return true;
}
if (strName.size()>11 && strName.substr(strName.size() - 11, 11) == ".oc.b32.i2p") {
std::vector<unsigned char> vchAddr = DecodeBase32(strName.substr(0, strName.size() - 11).c_str());
if (vchAddr.size() != 16-sizeof(pchGarliCat))
// Standard I2P b32 address: <52 base32 chars>.b32.i2p
// (SHA-256 hash of destination key, base32-encoded)
if (strName.size()>7 && strName.substr(strName.size() - 7, 7) == ".b32.i2p") {
std::string b32Part = strName.substr(0, strName.size() - 7);
std::vector<unsigned char> vchAddr = DecodeBase32(b32Part.c_str());
if (vchAddr.size() == 32) {
// Standard 32-byte I2P destination hash
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
// Store as many bytes as fit (16 - prefix_size)
for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat) && i < vchAddr.size(); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
return true;
}
// Also handle the legacy .oc.b32.i2p format (10 bytes)
if (vchAddr.size() == 16 - sizeof(pchGarliCat)) {
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
return true;
}
}
// Modern I2P base32 address: 52 base32 chars = SHA-256(destination) (32 bytes)
// rendered as "<b32>.b32.i2p". Store the hash and flag this as an I2P address.
if (strName.size()>8 && strName.substr(strName.size() - 8, 8) == ".b32.i2p") {
std::string addrPart = strName.substr(0, strName.size() - 8);
std::vector<unsigned char> vchAddr = DecodeBase32(addrPart.c_str());
if (vchAddr.size() != 32)
return false;
memcpy(ip, pchOnionCat, sizeof(pchGarliCat));
for (unsigned int i=0; i<16-sizeof(pchGarliCat); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
// Keep the GarliCat prefix in ip[] so legacy reachability checks that
// look for unique-local space still treat this as a routable overlay.
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
memset(ip + sizeof(pchGarliCat), 0, 16 - sizeof(pchGarliCat));
memcpy(tor_v3_pubkey, vchAddr.data(), 32);
m_is_i2p = true;
m_is_tor_v3 = false;
return true;
}
return false;
@@ -797,7 +874,7 @@ bool CNetAddr::IsTorV3() const
bool CNetAddr::IsI2P() const
{
return (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0);
return m_is_i2p || (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0);
}
bool CNetAddr::IsLocal() const
@@ -903,8 +980,15 @@ std::string CNetAddr::ToStringIP() const
}
if (IsTor())
return EncodeBase32(&ip[6], 10) + ".onion";
if (m_is_i2p) {
// Modern I2P: base32 of the 32-byte destination hash, unpadded.
std::string b32 = EncodeBase32(tor_v3_pubkey, 32);
while (!b32.empty() && b32[b32.size() - 1] == '=')
b32.erase(b32.size() - 1);
return b32 + ".b32.i2p";
}
if (IsI2P())
return EncodeBase32(&ip[6], 10) + ".oc.b32.i2p";
return EncodeBase32(&ip[6], 10) + ".b32.i2p";
CService serv(*this, 0);
#ifdef USE_IPV6
struct sockaddr_storage sockaddr;
@@ -936,12 +1020,14 @@ bool operator==(const CNetAddr& a, const CNetAddr& b)
{
if (a.m_is_tor_v3 || b.m_is_tor_v3)
return a.m_is_tor_v3 == b.m_is_tor_v3 && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0;
if (a.m_is_i2p || b.m_is_i2p)
return a.m_is_i2p == b.m_is_i2p && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0;
return (memcmp(a.ip, b.ip, 16) == 0);
}
bool operator!=(const CNetAddr& a, const CNetAddr& b)
{
return (memcmp(a.ip, b.ip, 16) != 0);
return !(a == b);
}
bool operator<(const CNetAddr& a, const CNetAddr& b)
@@ -950,6 +1036,10 @@ bool operator<(const CNetAddr& a, const CNetAddr& b)
return !a.m_is_tor_v3; // non-v3 sorts before v3
if (a.m_is_tor_v3)
return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0;
if (a.m_is_i2p != b.m_is_i2p)
return !a.m_is_i2p; // non-i2p sorts before i2p
if (a.m_is_i2p)
return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0;
return (memcmp(a.ip, b.ip, 16) < 0);
}
@@ -973,6 +1063,17 @@ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const
// no two connections will be attempted to addresses with the same group
std::vector<unsigned char> CNetAddr::GetGroup() const
{
// Modern I2P addresses keep their identifying bytes in the 32-byte
// destination-hash field (ip[] only holds the overlay prefix), so derive
// the group from the hash to keep peers in distinct groups.
if (m_is_i2p) {
std::vector<unsigned char> vch;
vch.push_back(NET_I2P);
vch.push_back(tor_v3_pubkey[0]);
vch.push_back(tor_v3_pubkey[1]);
return vch;
}
std::vector<unsigned char> vchRet;
int nClass = NET_IPV6;
int nStartByte = 0;
@@ -1047,7 +1148,7 @@ std::vector<unsigned char> CNetAddr::GetGroup() const
uint64_t CNetAddr::GetHash() const
{
uint256 hash;
if (m_is_tor_v3)
if (m_is_tor_v3 || m_is_i2p)
hash = Hash(&tor_v3_pubkey[0], &tor_v3_pubkey[32]);
else
hash = Hash(&ip[0], &ip[16]);
@@ -1312,3 +1413,151 @@ void CService::SetPort(unsigned short portIn)
{
port = portIn;
}
// ═══════════════════════════════════════════════════════════════════════════════
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
// See netbase.h for the contract. These are intentionally free of SSL/Tor
// dependencies so they can be unit-tested in isolation.
// ═══════════════════════════════════════════════════════════════════════════════
bool IsValidSocksNegotiationTimeout(int nMs)
{
// Range bounds match the documented -torconnecttimeout contract. 5000ms
// is the lower edge that still tolerates a slow SOCKS handshake over a
// congested link; 180000ms (3 min) is the upper edge to prevent a stuck
// thread from holding an outbound connection slot indefinitely. These
// constants are duplicated in src/init.cpp's HelpMessage text and the
// test suite — keep all three in sync.
return nMs >= 5000 && nMs <= 180000;
}
int DechunkTransferEncoding(const std::string& body, std::string& decoded)
{
decoded.clear();
if (body.empty())
return DECHUNK_EMPTY;
// HTTP chunked framing requires every chunk-size line to be terminated
// by CRLF. We walk the body one chunk at a time and validate each piece.
// The previous implementation silently dropped malformed chunks and
// treated them as the last-chunk marker, which lost the entire seed list
// for any non-conforming server. This version returns an explicit error
// code for each failure mode.
size_t pos = 0;
const size_t n = body.size();
bool sawLastChunk = false;
while (pos < n) {
// Find end of chunk-size line. Required: CRLF.
size_t eol = body.find("\r\n", pos);
if (eol == std::string::npos)
return DECHUNK_NO_CHUNK_TERMINATOR;
std::string sizeLine = body.substr(pos, eol - pos);
pos = eol + 2; // consume CRLF
// Strip chunk extensions per RFC 7230 §4.1.1: ";name[=value]" after
// the hex size. Extensions are part of the framing protocol, not
// data, so we drop them here.
size_t semi = sizeLine.find(';');
std::string hexSize = (semi == std::string::npos) ? sizeLine : sizeLine.substr(0, semi);
// Strict hex validation: every character must be [0-9A-Fa-f]. Empty
// size lines (e.g. a stray CRLF) are rejected as malformed, not
// silently treated as 0. strtoul alone would also accept leading
// whitespace, '+', and '-' which we don't want.
if (hexSize.empty())
return DECHUNK_INVALID_HEX;
for (size_t i = 0; i < hexSize.size(); ++i) {
if (!isxdigit(static_cast<unsigned char>(hexSize[i])))
return DECHUNK_INVALID_HEX;
}
// strtoul returns ULONG_MAX on overflow. We also need to guard
// against chunks larger than the remaining input, which the old
// code clamped silently. Use strtoull so we can detect overflow
// without truncation surprises on 32-bit builds.
errno = 0;
char* endp = nullptr;
unsigned long long chunkSize = strtoull(hexSize.c_str(), &endp, 16);
if (errno == ERANGE || chunkSize > std::numeric_limits<size_t>::max())
return DECHUNK_INVALID_HEX;
if (endp == hexSize.c_str())
return DECHUNK_INVALID_HEX;
if (chunkSize == 0) {
// Last-chunk: payload is empty, trailer part (which we ignore)
// follows and is terminated by a final CRLF on its own line.
sawLastChunk = true;
break;
}
// Bounds check before reading the chunk data. Catching this
// explicitly (rather than clamping) is what lets callers
// distinguish "truncated network read" from "server sent us junk".
if (chunkSize > n - pos)
return DECHUNK_OVERSIZE_CHUNK;
decoded.append(body, pos, static_cast<size_t>(chunkSize));
pos += static_cast<size_t>(chunkSize);
// Per RFC 7230 each chunk's data must be followed by a CRLF. We
// tolerate the final chunk missing its trailing CRLF (some clients
// do this when the connection is being closed anyway), but for any
// non-final chunk a missing CRLF is a hard framing error.
if (pos + 1 < n && body[pos] == '\r' && body[pos + 1] == '\n') {
pos += 2;
} else if (pos >= n) {
// End of input immediately after chunk data — no CRLF, but
// nothing left to misframe. Reject to be strict.
return DECHUNK_MISSING_DATA_CRLF;
} else {
return DECHUNK_MISSING_DATA_CRLF;
}
}
if (!sawLastChunk) {
// Body ended without a last-chunk marker. Treat as malformed
// rather than accepting a truncated body.
return DECHUNK_NO_CHUNK_TERMINATOR;
}
return DECHUNK_OK;
}
std::vector<std::string> ParseSeedListBody(const std::string& body)
{
std::vector<std::string> out;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line)) {
// Strip inline '#' comments. Per common seed-list convention, the
// first '#' to end-of-line is comment.
size_t hashPos = line.find('#');
if (hashPos != std::string::npos)
line = line.substr(0, hashPos);
// Split on whitespace, comma, or semicolon so multiple addresses
// on one line are all captured. CR/LF are already consumed by
// std::getline but a trailing CR (LF-only line endings) is trimmed
// implicitly by skipping it as a separator below.
size_t start = 0;
while (start <= line.size()) {
size_t sep = line.find_first_of(" \t,;", start);
std::string tok = (sep == std::string::npos)
? line.substr(start)
: line.substr(start, sep - start);
// Trim CR and any leftover whitespace from the token. The
// 'sep' loop above eats spaces/tabs but a bare CR survives.
while (!tok.empty() && (tok.back() == '\r' || tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
if (!tok.empty())
out.push_back(tok);
if (sep == std::string::npos) break;
start = sep + 1;
}
}
return out;
}
+76 -1
View File
@@ -32,13 +32,86 @@ extern int nConnectTimeout;
extern int nSocksNegotiationTimeout;
extern bool fNameLookup;
// ═══════════════════════════════════════════════════════════════════════════════
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
// Extracted from net.cpp ThreadHTTPSeedFetch2 so they can be unit-tested
// without the SSL/Tor network stack. All functions are side-effect free and
// operate on std::string/std::vector<std::string> only.
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Result of dechunking an HTTP/1.1 chunked body. The daemon used to silently
* treat malformed framing as a zero-length chunk, which dropped the entire
* seed list. This enum lets the caller distinguish each failure mode and
* surface it in logs.
*/
enum DechunkResult {
DECHUNK_OK = 0, // success
DECHUNK_EMPTY, // body is empty
DECHUNK_NO_CHUNK_TERMINATOR, // missing CRLF after a chunk-size line
DECHUNK_INVALID_HEX, // chunk-size line is not valid hex
DECHUNK_OVERSIZE_CHUNK, // declared chunk size exceeds remaining input
DECHUNK_MISSING_DATA_CRLF, // CRLF missing after a chunk's data
};
/**
* Decode an HTTP/1.1 Transfer-Encoding: chunked body.
*
* chunked-body = *chunk last-chunk trailer-part CRLF
* chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
* chunk-size = 1*HEXDIG
* last-chunk = 1*("0") [ chunk-ext ] CRLF
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
*
* @param[in] body the raw body bytes after the header terminator
* @param[out] decoded the dechunked payload on success
* @return status code (DECHUNK_OK or one of the failure modes)
*
* The implementation is intentionally strict: a malformed hex digit, a
* missing CRLF, or a chunk whose declared size is larger than the remaining
* input all return an explicit error code rather than silently clamping.
* Chunk extensions ("a;foo=bar") are preserved (stripped from the size
* line) so legitimate servers that attach metadata to chunks are still
* accepted.
*/
int DechunkTransferEncoding(const std::string& body, std::string& decoded);
/**
* Parse a tolerant HTTPS seed-list body into individual host entries.
*
* Accepted per line:
* - one or more addresses separated by whitespace, commas, or semicolons
* - inline "#" comments (everything after '#' is dropped)
* - blank lines
* - CRLF or LF line endings
*
* Each returned entry is the address string (e.g. "abcd...onion:24112" or
* "abcd...onion"). Empty/whitespace-only entries are omitted. The result is
* a list of candidate strings suitable for CNetAddr/CService validation
* downstream.
*/
std::vector<std::string> ParseSeedListBody(const std::string& body);
/**
* Validate the -torconnecttimeout / nSocksNegotiationTimeout value.
*
* Accepts 5000..180000 ms inclusive. Returns true for in-range, false for
* out-of-range. This is the central policy so callers and tests stay in
* sync; do not duplicate the literal numbers elsewhere.
*/
bool IsValidSocksNegotiationTimeout(int nMs);
/** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */
class CNetAddr
{
protected:
unsigned char ip[16]; // in network byte order
unsigned char tor_v3_pubkey[32]; // Ed25519 public key for Tor v3 onion addresses
// For Tor v3 this holds the 32-byte Ed25519 public key. When m_is_i2p is
// set it instead holds the 32-byte SHA-256 of the I2P destination (the
// value rendered as the ".b32.i2p" address). A CNetAddr is never both.
unsigned char tor_v3_pubkey[32];
bool m_is_tor_v3;
bool m_is_i2p;
public:
CNetAddr();
@@ -91,6 +164,7 @@ class CNetAddr
READWRITE(FLATDATA(ip));
READWRITE(FLATDATA(tor_v3_pubkey));
READWRITE(m_is_tor_v3);
READWRITE(m_is_i2p);
)
};
@@ -134,6 +208,7 @@ class CService : public CNetAddr
READWRITE(FLATDATA(ip));
READWRITE(FLATDATA(tor_v3_pubkey));
READWRITE(m_is_tor_v3);
READWRITE(m_is_i2p);
unsigned short portN = htons(port);
READWRITE(portN);
if (fRead)
+2
View File
@@ -4,6 +4,8 @@
// Hardcoded onion seed nodes for initial peer discovery.
// Also fetched dynamically via https://seeds.cryptographic-triangles.org/seeds.txt
static const char *strMainNetOnionSeed[][1] = {
// SAMI-PC - authoritative wallet node (main PC)
{"6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion"},
// DNS2 - primary bootstrap server (194.233.88.206)
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
// DNS3 - canonical chain reference (74.208.167.19)
+12
View File
@@ -72,6 +72,18 @@ enum
NODE_SNAPSHOT = (1 << 1), // peer can serve UTXO snapshot chunks
};
/** Inventory type constants for CInv.
*
* MSG_TX and MSG_BLOCK are the legacy inventory types used for
* transaction and block relay. MSG_CMPCT_BLOCK (BIP152) signals
* that the sender wants the block delivered as a compact block
* instead of a full serialized block.
*/
enum
{
MSG_CMPCT_BLOCK = 4, // BIP152 compact block inventory type
};
/** A CService with information about it as peer */
class CAddress : public CService
{
+142 -22
View File
@@ -1366,13 +1366,13 @@ QPushButton:hover {
<property name="minimumSize">
<size>
<width>0</width>
<height>37</height>
<height>52</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>37</height>
<height>52</height>
</size>
</property>
<property name="styleSheet">
@@ -1413,26 +1413,146 @@ QLabel {
</spacer>
</item>
<item>
<widget class="QLabel" name="label_onion">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .onion address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
<widget class="QWidget" name="wAddressStack" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="wI2PRow" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_i2p">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_i2p_icon">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>I2P router status</string>
</property>
<property name="text">
<string notr="true">[I2P]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_i2p">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .b32.i2p address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="wTorRow" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_tor">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_tor_icon">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Tor V3 hidden service status</string>
</property>
<property name="text">
<string notr="true">[Tor]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_onion">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .onion address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+1 -1
View File
@@ -815,7 +815,7 @@ QWidget#line {
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1416,7 +1416,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1444,7 +1444,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1429,7 +1429,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1457,7 +1457,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1414,7 +1414,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1442,7 +1442,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1427,7 +1427,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1455,7 +1455,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>

Some files were not shown because too many files have changed in this diff Show More