Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41ba9f8bc9 | |||
| cbb189aade | |||
| 5635cb5e57 | |||
| b6feab8e94 | |||
| 333f7abfc0 | |||
| eb20edf890 | |||
| ff7a7d3b6b | |||
| fb5f1be032 | |||
| 83a66814b0 | |||
| ae7beb0df7 | |||
| d4d0ddf849 | |||
| 3566eed9e1 | |||
| 3473e80876 | |||
| a48fb88e4c | |||
| bfdb399772 | |||
| c577fb2ff5 | |||
| b6b3f3877f | |||
| 9ed79d53a6 | |||
| 8615e6b46d | |||
| e694a189f8 | |||
| 2aeae07d0b | |||
| fcfa3b9938 | |||
| 01f3fdf2ff | |||
| baa9e0a650 | |||
| ba9cb89a97 | |||
| ede72d8e8a | |||
| 8e6c6b36bf | |||
| 045bc36716 | |||
| a55e45ac1a | |||
| bfb422417d | |||
| 7e359c0f21 | |||
| ba3d7a766a | |||
| e16d3b2fb2 | |||
| ba9a825ea4 | |||
| 1ec7306e1d | |||
| 63e33a1569 | |||
| 249c60eebe | |||
| 50973e22f7 | |||
| d2c1033d8a | |||
| fb07d50235 | |||
| b623396186 | |||
| 7c67a54a1d | |||
| d308044690 | |||
| 42639ac600 | |||
| b7e7f56a30 | |||
| 34f65eb836 | |||
| 9052b79ef6 | |||
| cf2ff6768d | |||
| 5973ee7ef7 | |||
| a25b29ef99 | |||
| 91453deb46 | |||
| dcb27aa8f2 | |||
| dca34a02bb | |||
| 53c9654caf | |||
| 0c6a2223cb | |||
| c7768fd42e | |||
| c2257bb827 | |||
| 4f452514dc | |||
| e07a90d7d1 | |||
| 407355afb0 | |||
| eb1851ba89 | |||
| 75dd9e034a | |||
| bf401437e8 | |||
| 518de7cb2e | |||
| c5f55fe802 | |||
| 16224898d4 | |||
| 28f5fcdbca | |||
| aa1851dd6a | |||
| 53f003aef1 | |||
| 9762c741b7 | |||
| 6726365872 | |||
| 20fc2ee6dd | |||
| 5d9a0f47f9 | |||
| 7f309800e5 | |||
| ff0eeaac89 | |||
| 6cf30350ea | |||
| c1c9f19870 | |||
| 77b05a84f2 | |||
| 2e19d85b18 | |||
| b9ce72d39a | |||
| 6defb54300 | |||
| 43eaa96bc9 |
+199
-22
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
Name: triangles
|
||||
Version: 5.7.6
|
||||
Version: 6.1.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
|
||||
License: MIT
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+86
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Executable
+46
@@ -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 .."
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
Submodule
+1
Submodule src/i2p/i2pd-src added at 8497a429dc
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -815,7 +815,7 @@ QWidget#line {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></string>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1416,7 +1416,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1444,7 +1444,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1429,7 +1429,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1457,7 +1457,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1414,7 +1414,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1442,7 +1442,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1427,7 +1427,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1455,7 +1455,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://triangles.technology"> &#187; TRI home</a></body></source>
|
||||
<a href="https://cryptographic-triangles.org/"> &#187; TRI home</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></source>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user